summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/Makefile2
-rw-r--r--nixos/doc/manual/configuration/config-file.xml7
-rw-r--r--nixos/doc/manual/configuration/ipv6-config.xml8
-rw-r--r--nixos/doc/manual/configuration/luks-file-systems.xml6
-rw-r--r--nixos/doc/manual/configuration/modularity.xml2
-rw-r--r--nixos/doc/manual/configuration/x-windows.xml59
-rw-r--r--nixos/doc/manual/development/settings-options.xml14
-rw-r--r--nixos/doc/manual/development/writing-documentation.xml3
-rw-r--r--nixos/doc/manual/development/writing-modules.xml5
-rw-r--r--nixos/doc/manual/development/writing-nixos-tests.xml33
-rw-r--r--nixos/doc/manual/installation/installing.xml2
-rw-r--r--nixos/doc/manual/release-notes/rl-2009.xml8
-rw-r--r--nixos/doc/manual/release-notes/rl-2105.xml327
-rw-r--r--nixos/lib/make-disk-image.nix78
-rw-r--r--nixos/lib/make-options-doc/default.nix26
-rw-r--r--nixos/lib/make-squashfs.nix2
-rw-r--r--nixos/lib/qemu-flags.nix4
-rw-r--r--nixos/lib/test-driver/test-driver.py82
-rw-r--r--nixos/lib/testing-python.nix22
-rw-r--r--nixos/maintainers/scripts/cloudstack/cloudstack-image.nix1
-rw-r--r--nixos/maintainers/scripts/ec2/amazon-image.nix3
-rwxr-xr-xnixos/maintainers/scripts/gce/create-gce.sh12
-rw-r--r--nixos/maintainers/scripts/openstack/openstack-image.nix2
-rw-r--r--nixos/modules/config/iproute2.nix18
-rw-r--r--nixos/modules/config/pulseaudio.nix1
-rw-r--r--nixos/modules/config/system-path.nix20
-rw-r--r--nixos/modules/config/update-users-groups.pl2
-rw-r--r--nixos/modules/config/users-groups.nix72
-rw-r--r--nixos/modules/hardware/all-firmware.nix2
-rw-r--r--nixos/modules/hardware/keyboard/teck.nix16
-rw-r--r--nixos/modules/hardware/printers.nix2
-rw-r--r--nixos/modules/hardware/rtl-sdr.nix7
-rw-r--r--nixos/modules/hardware/sata.nix100
-rw-r--r--nixos/modules/hardware/system-76.nix3
-rw-r--r--nixos/modules/hardware/ubertooth.nix29
-rw-r--r--nixos/modules/hardware/video/amdgpu.nix9
-rw-r--r--nixos/modules/i18n/input-method/default.nix3
-rw-r--r--nixos/modules/i18n/input-method/default.xml22
-rw-r--r--nixos/modules/i18n/input-method/kime.nix49
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix53
-rw-r--r--nixos/modules/installer/sd-card/sd-image-aarch64.nix7
-rw-r--r--nixos/modules/installer/sd-card/sd-image.nix10
-rw-r--r--nixos/modules/installer/tools/nixos-install.sh2
-rw-r--r--nixos/modules/installer/tools/tools.nix5
-rw-r--r--nixos/modules/misc/documentation.nix2
-rw-r--r--nixos/modules/misc/ids.nix4
-rw-r--r--nixos/modules/misc/meta.nix4
-rw-r--r--nixos/modules/module-list.nix46
-rw-r--r--nixos/modules/profiles/all-hardware.nix57
-rw-r--r--nixos/modules/programs/bash/bash-completion.nix37
-rw-r--r--nixos/modules/programs/bash/bash.nix45
-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/ccache.nix2
-rw-r--r--nixos/modules/programs/command-not-found/command-not-found.nix4
-rw-r--r--nixos/modules/programs/command-not-found/command-not-found.pl2
-rw-r--r--nixos/modules/programs/droidcam.nix16
-rw-r--r--nixos/modules/programs/feedbackd.nix32
-rw-r--r--nixos/modules/programs/fish.nix18
-rw-r--r--nixos/modules/programs/fish_completion-generator.patch19
-rw-r--r--nixos/modules/programs/flexoptix-app.nix25
-rw-r--r--nixos/modules/programs/less.nix2
-rw-r--r--nixos/modules/programs/mininet.nix2
-rw-r--r--nixos/modules/programs/partition-manager.nix19
-rw-r--r--nixos/modules/programs/phosh.nix167
-rw-r--r--nixos/modules/programs/steam.nix33
-rw-r--r--nixos/modules/programs/sway.nix2
-rw-r--r--nixos/modules/programs/turbovnc.nix54
-rw-r--r--nixos/modules/rename.nix1
-rw-r--r--nixos/modules/security/acme.nix52
-rw-r--r--nixos/modules/services/audio/jmusicbot.nix41
-rw-r--r--nixos/modules/services/audio/mpd.nix4
-rw-r--r--nixos/modules/services/audio/snapserver.nix17
-rw-r--r--nixos/modules/services/audio/spotifyd.nix1
-rw-r--r--nixos/modules/services/backup/borgbackup.nix1
-rw-r--r--nixos/modules/services/backup/borgmatic.nix57
-rw-r--r--nixos/modules/services/backup/restic.nix13
-rw-r--r--nixos/modules/services/cluster/k3s/default.nix7
-rw-r--r--nixos/modules/services/cluster/kubernetes/addons/dns.nix7
-rw-r--r--nixos/modules/services/cluster/kubernetes/apiserver.nix44
-rw-r--r--nixos/modules/services/cluster/kubernetes/controller-manager.nix2
-rw-r--r--nixos/modules/services/cluster/kubernetes/default.nix33
-rw-r--r--nixos/modules/services/cluster/kubernetes/flannel.nix40
-rw-r--r--nixos/modules/services/cluster/kubernetes/kubelet.nix43
-rw-r--r--nixos/modules/services/cluster/kubernetes/pki.nix1
-rw-r--r--nixos/modules/services/cluster/kubernetes/proxy.nix2
-rw-r--r--nixos/modules/services/cluster/kubernetes/scheduler.nix2
-rw-r--r--nixos/modules/services/computing/slurm/slurm.nix15
-rw-r--r--nixos/modules/services/continuous-integration/buildkite-agents.nix10
-rw-r--r--nixos/modules/services/continuous-integration/github-runner.nix299
-rw-r--r--nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix28
-rw-r--r--nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix18
-rw-r--r--nixos/modules/services/continuous-integration/hydra/default.nix29
-rw-r--r--nixos/modules/services/databases/cassandra.nix411
-rw-r--r--nixos/modules/services/databases/couchdb.nix19
-rw-r--r--nixos/modules/services/databases/pgmanage.nix1
-rw-r--r--nixos/modules/services/databases/postgresql.nix22
-rw-r--r--nixos/modules/services/databases/redis.nix62
-rw-r--r--nixos/modules/services/desktops/geoclue2.nix4
-rw-r--r--nixos/modules/services/desktops/pipewire/alsa-monitor.conf.json34
-rw-r--r--nixos/modules/services/desktops/pipewire/bluez-monitor.conf.json36
-rw-r--r--nixos/modules/services/desktops/pipewire/client-rt.conf.json39
-rw-r--r--nixos/modules/services/desktops/pipewire/client.conf.json31
-rw-r--r--nixos/modules/services/desktops/pipewire/jack.conf.json28
-rw-r--r--nixos/modules/services/desktops/pipewire/media-session.conf.json67
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire-media-session.nix356
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire-pulse.conf.json41
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire.conf.json82
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire.nix213
-rw-r--r--nixos/modules/services/desktops/pipewire/v4l2-monitor.conf.json30
-rw-r--r--nixos/modules/services/display-managers/greetd.nix106
-rw-r--r--nixos/modules/services/games/factorio.nix10
-rw-r--r--nixos/modules/services/games/minetest-server.nix2
-rw-r--r--nixos/modules/services/games/quake3-server.nix111
-rw-r--r--nixos/modules/services/hardware/acpid.nix31
-rw-r--r--nixos/modules/services/hardware/fancontrol.nix28
-rw-r--r--nixos/modules/services/hardware/pcscd.nix86
-rw-r--r--nixos/modules/services/hardware/sane.nix17
-rw-r--r--nixos/modules/services/hardware/spacenavd.nix2
-rw-r--r--nixos/modules/services/hardware/tcsd.nix35
-rw-r--r--nixos/modules/services/logging/graylog.nix6
-rw-r--r--nixos/modules/services/logging/logstash.nix3
-rw-r--r--nixos/modules/services/logging/promtail.nix1
-rw-r--r--nixos/modules/services/logging/vector.nix43
-rw-r--r--nixos/modules/services/mail/dovecot.nix2
-rw-r--r--nixos/modules/services/mail/exim.nix9
-rw-r--r--nixos/modules/services/mail/mailman.nix8
-rw-r--r--nixos/modules/services/mail/nullmailer.nix1
-rw-r--r--nixos/modules/services/mail/opendkim.nix2
-rw-r--r--nixos/modules/services/mail/postfix.nix16
-rw-r--r--nixos/modules/services/mail/rspamd.nix2
-rw-r--r--nixos/modules/services/mail/spamassassin.nix65
-rw-r--r--nixos/modules/services/misc/airsonic.nix2
-rw-r--r--nixos/modules/services/misc/apache-kafka.nix23
-rw-r--r--nixos/modules/services/misc/bazarr.nix1
-rw-r--r--nixos/modules/services/misc/defaultUnicornConfig.rb69
-rw-r--r--nixos/modules/services/misc/disnix.nix99
-rw-r--r--nixos/modules/services/misc/duckling.nix39
-rw-r--r--nixos/modules/services/misc/dysnomia.nix259
-rw-r--r--nixos/modules/services/misc/etebase-server.nix173
-rw-r--r--nixos/modules/services/misc/gitea.nix84
-rw-r--r--nixos/modules/services/misc/gitlab.nix593
-rw-r--r--nixos/modules/services/misc/gitlab.xml57
-rw-r--r--nixos/modules/services/misc/gollum.nix2
-rw-r--r--nixos/modules/services/misc/home-assistant.nix92
-rw-r--r--nixos/modules/services/misc/jellyfin.nix22
-rw-r--r--nixos/modules/services/misc/lifecycled.nix164
-rw-r--r--nixos/modules/services/misc/mame.nix2
-rw-r--r--nixos/modules/services/misc/matrix-appservice-irc.nix229
-rw-r--r--nixos/modules/services/misc/matrix-dendrite.nix181
-rw-r--r--nixos/modules/services/misc/matrix-synapse.xml6
-rw-r--r--nixos/modules/services/misc/mautrix-telegram.nix20
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix1
-rw-r--r--nixos/modules/services/misc/nix-gc.nix53
-rw-r--r--nixos/modules/services/misc/ombi.nix81
-rw-r--r--nixos/modules/services/misc/packagekit.nix91
-rw-r--r--nixos/modules/services/misc/pinnwand.nix69
-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/redmine.nix2
-rw-r--r--nixos/modules/services/misc/zigbee2mqtt.nix96
-rw-r--r--nixos/modules/services/monitoring/alerta.nix4
-rw-r--r--nixos/modules/services/monitoring/datadog-agent.nix4
-rw-r--r--nixos/modules/services/monitoring/grafana.nix152
-rw-r--r--nixos/modules/services/monitoring/nagios.nix2
-rw-r--r--nixos/modules/services/monitoring/netdata.nix29
-rw-r--r--nixos/modules/services/monitoring/prometheus/default.nix4
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix31
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix59
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix82
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/domain.nix19
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix40
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/knot.nix50
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/openldap.nix67
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/postgres.nix37
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/systemd.nix18
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/unbound.nix59
-rw-r--r--nixos/modules/services/monitoring/scollector.nix2
-rw-r--r--nixos/modules/services/monitoring/tuptime.nix5
-rw-r--r--nixos/modules/services/monitoring/vnstat.nix28
-rw-r--r--nixos/modules/services/monitoring/zabbix-agent.nix11
-rw-r--r--nixos/modules/services/network-filesystems/ceph.nix2
-rw-r--r--nixos/modules/services/network-filesystems/ipfs.nix32
-rw-r--r--nixos/modules/services/network-filesystems/samba.nix1
-rw-r--r--nixos/modules/services/networking/adguardhome.nix78
-rw-r--r--nixos/modules/services/networking/babeld.nix20
-rw-r--r--nixos/modules/services/networking/bind.nix78
-rw-r--r--nixos/modules/services/networking/bird.nix15
-rw-r--r--nixos/modules/services/networking/cjdns.nix2
-rw-r--r--nixos/modules/services/networking/consul.nix2
-rw-r--r--nixos/modules/services/networking/croc.nix88
-rw-r--r--nixos/modules/services/networking/dnsdist.nix2
-rw-r--r--nixos/modules/services/networking/doh-proxy-rust.nix60
-rw-r--r--nixos/modules/services/networking/flannel.nix6
-rw-r--r--nixos/modules/services/networking/gobgpd.nix64
-rw-r--r--nixos/modules/services/networking/gvpe.nix2
-rw-r--r--nixos/modules/services/networking/inspircd.nix62
-rw-r--r--nixos/modules/services/networking/ircd-hybrid/default.nix2
-rw-r--r--nixos/modules/services/networking/iscsi/initiator.nix84
-rw-r--r--nixos/modules/services/networking/iscsi/root-initiator.nix181
-rw-r--r--nixos/modules/services/networking/iscsi/target.nix53
-rw-r--r--nixos/modules/services/networking/kresd.nix30
-rw-r--r--nixos/modules/services/networking/libreswan.nix8
-rw-r--r--nixos/modules/services/networking/mosquitto.nix46
-rw-r--r--nixos/modules/services/networking/mullvad-vpn.nix2
-rw-r--r--nixos/modules/services/networking/mxisd.nix4
-rw-r--r--nixos/modules/services/networking/ncdns.nix6
-rw-r--r--nixos/modules/services/networking/nebula.nix219
-rw-r--r--nixos/modules/services/networking/networkmanager.nix4
-rw-r--r--nixos/modules/services/networking/nomad.nix2
-rw-r--r--nixos/modules/services/networking/openvpn.nix2
-rw-r--r--nixos/modules/services/networking/pixiecore.nix1
-rw-r--r--nixos/modules/services/networking/pleroma.nix1
-rw-r--r--nixos/modules/services/networking/privoxy.nix304
-rw-r--r--nixos/modules/services/networking/quagga.nix185
-rw-r--r--nixos/modules/services/networking/rxe.nix4
-rw-r--r--nixos/modules/services/networking/searx.nix14
-rw-r--r--nixos/modules/services/networking/spacecookie.nix161
-rw-r--r--nixos/modules/services/networking/sslh.nix2
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/module.nix2
-rw-r--r--nixos/modules/services/networking/strongswan.nix2
-rw-r--r--nixos/modules/services/networking/tailscale.nix1
-rw-r--r--nixos/modules/services/networking/unbound.nix253
-rw-r--r--nixos/modules/services/networking/wg-quick.nix8
-rw-r--r--nixos/modules/services/networking/wireguard.nix21
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix28
-rw-r--r--nixos/modules/services/networking/zerobin.nix2
-rw-r--r--nixos/modules/services/printing/cupsd.nix2
-rw-r--r--nixos/modules/services/security/fail2ban.nix20
-rw-r--r--nixos/modules/services/security/fprintd.nix30
-rw-r--r--nixos/modules/services/security/oauth2_proxy.nix6
-rw-r--r--nixos/modules/services/security/oauth2_proxy_nginx.nix5
-rw-r--r--nixos/modules/services/security/privacyidea.nix29
-rw-r--r--nixos/modules/services/security/sshguard.nix36
-rw-r--r--nixos/modules/services/security/step-ca.nix134
-rw-r--r--nixos/modules/services/system/cloud-init.nix2
-rw-r--r--nixos/modules/services/system/localtime.nix9
-rw-r--r--nixos/modules/services/torrent/transmission.nix1
-rw-r--r--nixos/modules/services/ttys/getty.nix20
-rw-r--r--nixos/modules/services/wayland/cage.nix2
-rw-r--r--nixos/modules/services/web-apps/bookstack.nix368
-rw-r--r--nixos/modules/services/web-apps/calibre-web.nix165
-rw-r--r--nixos/modules/services/web-apps/discourse.nix1037
-rw-r--r--nixos/modules/services/web-apps/discourse.xml323
-rw-r--r--nixos/modules/services/web-apps/dokuwiki.nix8
-rw-r--r--nixos/modules/services/web-apps/galene.nix6
-rw-r--r--nixos/modules/services/web-apps/hledger-web.nix109
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix6
-rw-r--r--nixos/modules/services/web-apps/mastodon.nix35
-rw-r--r--nixos/modules/services/web-apps/miniflux.nix24
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix65
-rw-r--r--nixos/modules/services/web-apps/nextcloud.xml2
-rw-r--r--nixos/modules/services/web-apps/shiori.nix4
-rw-r--r--nixos/modules/services/web-apps/whitebophir.nix9
-rw-r--r--nixos/modules/services/web-apps/wiki-js.nix139
-rw-r--r--nixos/modules/services/web-apps/wordpress.nix4
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix6
-rw-r--r--nixos/modules/services/web-servers/minio.nix11
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix56
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix13
-rw-r--r--nixos/modules/services/web-servers/pomerium.nix131
-rw-r--r--nixos/modules/services/web-servers/trafficserver.nix318
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome3.nix3
-rw-r--r--nixos/modules/services/x11/desktop-managers/kodi.nix14
-rw-r--r--nixos/modules/services/x11/desktop-managers/xfce.nix8
-rw-r--r--nixos/modules/services/x11/display-managers/default.nix22
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix8
-rw-r--r--nixos/modules/services/x11/window-managers/default.nix2
-rw-r--r--nixos/modules/services/x11/window-managers/e16.nix26
-rw-r--r--nixos/modules/services/x11/window-managers/wmderland.nix61
-rw-r--r--nixos/modules/services/x11/xserver.nix8
-rw-r--r--nixos/modules/system/activation/switch-to-configuration.pl2
-rw-r--r--nixos/modules/system/activation/top-level.nix7
-rw-r--r--nixos/modules/system/boot/initrd-openvpn.nix2
-rw-r--r--nixos/modules/system/boot/kernel_config.nix22
-rw-r--r--nixos/modules/system/boot/loader/grub/grub.nix8
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py56
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix15
-rw-r--r--nixos/modules/system/boot/plymouth.nix67
-rw-r--r--nixos/modules/system/boot/resolved.nix5
-rw-r--r--nixos/modules/system/boot/stage-1-init.sh15
-rw-r--r--nixos/modules/system/boot/stage-1.nix2
-rw-r--r--nixos/modules/system/boot/stage-2-init.sh3
-rw-r--r--nixos/modules/system/boot/stage-2.nix15
-rw-r--r--nixos/modules/system/boot/systemd.nix18
-rw-r--r--nixos/modules/system/etc/etc.nix2
-rw-r--r--nixos/modules/tasks/cpu-freq.nix2
-rw-r--r--nixos/modules/tasks/filesystems.nix6
-rw-r--r--nixos/modules/tasks/filesystems/zfs.nix5
-rw-r--r--nixos/modules/tasks/network-interfaces-scripted.nix18
-rw-r--r--nixos/modules/tasks/network-interfaces-systemd.nix2
-rw-r--r--nixos/modules/tasks/network-interfaces.nix104
-rw-r--r--nixos/modules/testing/service-runner.nix2
-rw-r--r--nixos/modules/virtualisation/amazon-init.nix51
-rw-r--r--nixos/modules/virtualisation/anbox.nix1
-rw-r--r--nixos/modules/virtualisation/azure-image.nix5
-rw-r--r--nixos/modules/virtualisation/brightbox-image.nix2
-rw-r--r--nixos/modules/virtualisation/containerd.nix60
-rw-r--r--nixos/modules/virtualisation/containers.nix57
-rw-r--r--nixos/modules/virtualisation/digital-ocean-image.nix5
-rw-r--r--nixos/modules/virtualisation/docker.nix1
-rw-r--r--nixos/modules/virtualisation/ec2-data.nix2
-rw-r--r--nixos/modules/virtualisation/ec2-metadata-fetcher.nix2
-rw-r--r--nixos/modules/virtualisation/gce-images.nix10
-rw-r--r--nixos/modules/virtualisation/google-compute-config.nix2
-rw-r--r--nixos/modules/virtualisation/google-compute-image.nix5
-rw-r--r--nixos/modules/virtualisation/hyperv-guest.nix2
-rw-r--r--nixos/modules/virtualisation/hyperv-image.nix5
-rw-r--r--nixos/modules/virtualisation/libvirtd.nix27
-rw-r--r--nixos/modules/virtualisation/lxd.nix61
-rw-r--r--nixos/modules/virtualisation/nixos-containers.nix43
-rw-r--r--nixos/modules/virtualisation/oci-containers.nix13
-rw-r--r--nixos/modules/virtualisation/openstack-metadata-fetcher.nix2
-rw-r--r--nixos/modules/virtualisation/podman.nix13
-rw-r--r--nixos/modules/virtualisation/virtualbox-image.nix5
-rw-r--r--nixos/modules/virtualisation/vmware-image.nix5
-rw-r--r--nixos/modules/virtualisation/xe-guest-utilities.nix2
-rw-r--r--nixos/modules/virtualisation/xen-dom0.nix5
-rw-r--r--nixos/release.nix21
-rw-r--r--nixos/tests/acme.nix54
-rw-r--r--nixos/tests/agda.nix3
-rw-r--r--nixos/tests/airsonic.nix32
-rw-r--r--nixos/tests/all-tests.nix33
-rw-r--r--nixos/tests/amazon-init-shell.nix40
-rw-r--r--nixos/tests/babeld.nix6
-rw-r--r--nixos/tests/bitwarden.nix1
-rw-r--r--nixos/tests/cage.nix18
-rw-r--r--nixos/tests/calibre-web.nix53
-rw-r--r--nixos/tests/ceph-multi-node.nix3
-rw-r--r--nixos/tests/ceph-single-node-bluestore.nix3
-rw-r--r--nixos/tests/ceph-single-node.nix3
-rw-r--r--nixos/tests/containers-imperative.nix20
-rw-r--r--nixos/tests/containers-nested.nix30
-rw-r--r--nixos/tests/couchdb.nix38
-rw-r--r--nixos/tests/croc.nix51
-rw-r--r--nixos/tests/custom-ca.nix10
-rw-r--r--nixos/tests/discourse.nix197
-rw-r--r--nixos/tests/docker-tools.nix112
-rw-r--r--nixos/tests/doh-proxy-rust.nix43
-rw-r--r--nixos/tests/dokuwiki.nix4
-rw-r--r--nixos/tests/dovecot.nix2
-rw-r--r--nixos/tests/elk.nix1
-rw-r--r--nixos/tests/etebase-server.nix50
-rw-r--r--nixos/tests/fancontrol.nix40
-rw-r--r--nixos/tests/gitdaemon.nix6
-rw-r--r--nixos/tests/gitea.nix2
-rw-r--r--nixos/tests/gitlab.nix163
-rw-r--r--nixos/tests/gobgpd.nix71
-rw-r--r--nixos/tests/hibernate.nix124
-rw-r--r--nixos/tests/hledger-web.nix25
-rw-r--r--nixos/tests/home-assistant.nix28
-rw-r--r--nixos/tests/inspircd.nix93
-rw-r--r--nixos/tests/installed-tests/default.nix1
-rw-r--r--nixos/tests/installed-tests/librsvg.nix9
-rw-r--r--nixos/tests/installed-tests/pipewire.nix10
-rw-r--r--nixos/tests/installer.nix15
-rw-r--r--nixos/tests/ipv6.nix85
-rw-r--r--nixos/tests/iscsi-root.nix161
-rw-r--r--nixos/tests/jellyfin.nix172
-rw-r--r--nixos/tests/kafka.nix6
-rw-r--r--nixos/tests/kernel-generic.nix37
-rw-r--r--nixos/tests/kernel-latest.nix17
-rw-r--r--nixos/tests/kernel-lts.nix17
-rw-r--r--nixos/tests/kernel-testing.nix17
-rw-r--r--nixos/tests/keycloak.nix1
-rw-r--r--nixos/tests/kubernetes/dns.nix15
-rw-r--r--nixos/tests/kubernetes/rbac.nix6
-rw-r--r--nixos/tests/matrix-appservice-irc.nix162
-rw-r--r--nixos/tests/miniflux.nix14
-rw-r--r--nixos/tests/mosquitto.nix5
-rw-r--r--nixos/tests/mxisd.nix17
-rw-r--r--nixos/tests/mysql/mariadb-galera-mariabackup.nix8
-rw-r--r--nixos/tests/mysql/mariadb-galera-rsync.nix6
-rw-r--r--nixos/tests/mysql/mysql.nix12
-rw-r--r--nixos/tests/nebula.nix223
-rw-r--r--nixos/tests/nextcloud/basic.nix22
-rw-r--r--nixos/tests/ombi.nix18
-rw-r--r--nixos/tests/os-prober.nix8
-rw-r--r--nixos/tests/packagekit.nix1
-rw-r--r--nixos/tests/php/default.nix21
-rw-r--r--nixos/tests/php/fpm.nix14
-rw-r--r--nixos/tests/php/httpd.nix7
-rw-r--r--nixos/tests/php/pcre.nix5
-rw-r--r--nixos/tests/pinnwand.nix10
-rw-r--r--nixos/tests/plikd.nix27
-rw-r--r--nixos/tests/podgrab.nix34
-rw-r--r--nixos/tests/podman.nix9
-rw-r--r--nixos/tests/pomerium.nix102
-rw-r--r--nixos/tests/privacyidea.nix12
-rw-r--r--nixos/tests/privoxy.nix113
-rw-r--r--nixos/tests/prometheus-exporters.nix548
-rw-r--r--nixos/tests/quagga.nix96
-rw-r--r--nixos/tests/redis.nix5
-rw-r--r--nixos/tests/restic.nix6
-rw-r--r--nixos/tests/rspamd.nix10
-rw-r--r--nixos/tests/searx.nix2
-rw-r--r--nixos/tests/shadow.nix3
-rw-r--r--nixos/tests/snapcast.nix4
-rw-r--r--nixos/tests/spacecookie.nix33
-rw-r--r--nixos/tests/spike.nix2
-rw-r--r--nixos/tests/sway.nix92
-rw-r--r--nixos/tests/systemd-confinement.nix1
-rw-r--r--nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix2
-rw-r--r--nixos/tests/systemd-unit-path.nix47
-rw-r--r--nixos/tests/trafficserver.nix176
-rw-r--r--nixos/tests/turbovnc-headless-server.nix171
-rw-r--r--nixos/tests/unbound.nix79
-rw-r--r--nixos/tests/wiki-js.nix152
-rw-r--r--nixos/tests/wireguard/basic.nix6
-rw-r--r--nixos/tests/wmderland.nix54
-rw-r--r--nixos/tests/zigbee2mqtt.nix6
411 files changed, 15211 insertions, 3242 deletions
diff --git a/nixos/doc/manual/Makefile b/nixos/doc/manual/Makefile
index b86a7600575..b2b6481b20c 100644
--- a/nixos/doc/manual/Makefile
+++ b/nixos/doc/manual/Makefile
@@ -1,5 +1,5 @@
 .PHONY: all
-all: manual-combined.xml format
+all: manual-combined.xml
 
 .PHONY: debug
 debug: generated manual-combined.xml
diff --git a/nixos/doc/manual/configuration/config-file.xml b/nixos/doc/manual/configuration/config-file.xml
index 7ccb5b3664e..19cfb57920d 100644
--- a/nixos/doc/manual/configuration/config-file.xml
+++ b/nixos/doc/manual/configuration/config-file.xml
@@ -16,9 +16,10 @@
   The first line (<literal>{ config, pkgs, ... }:</literal>) denotes that this
   is actually a function that takes at least the two arguments
   <varname>config</varname> and <varname>pkgs</varname>. (These are explained
-  later.) The function returns a <emphasis>set</emphasis> of option definitions
-  (<literal>{ <replaceable>...</replaceable> }</literal>). These definitions
-  have the form <literal><replaceable>name</replaceable> =
+  later, in chapter <xref linkend="sec-writing-modules" />) The function returns
+  a <emphasis>set</emphasis> of option definitions (<literal>{
+  <replaceable>...</replaceable> }</literal>). These definitions have the form
+  <literal><replaceable>name</replaceable> =
   <replaceable>value</replaceable></literal>, where
   <replaceable>name</replaceable> is the name of an option and
   <replaceable>value</replaceable> is its value. For example,
diff --git a/nixos/doc/manual/configuration/ipv6-config.xml b/nixos/doc/manual/configuration/ipv6-config.xml
index 7b89b4092be..45e85dbf3df 100644
--- a/nixos/doc/manual/configuration/ipv6-config.xml
+++ b/nixos/doc/manual/configuration/ipv6-config.xml
@@ -7,8 +7,12 @@
 
  <para>
   IPv6 is enabled by default. Stateless address autoconfiguration is used to
-  automatically assign IPv6 addresses to all interfaces. You can disable IPv6
-  support globally by setting:
+  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:
 <programlisting>
 <xref linkend="opt-networking.enableIPv6"/> = false;
 </programlisting>
diff --git a/nixos/doc/manual/configuration/luks-file-systems.xml b/nixos/doc/manual/configuration/luks-file-systems.xml
index 405a50a9e43..d8654d71ac0 100644
--- a/nixos/doc/manual/configuration/luks-file-systems.xml
+++ b/nixos/doc/manual/configuration/luks-file-systems.xml
@@ -26,7 +26,11 @@ Enter passphrase for /dev/disk/by-uuid/3f6b0024-3a44-4fde-a43a-767b872abe5d: ***
 
 <prompt># </prompt>mkfs.ext4 /dev/mapper/<replaceable>crypted</replaceable>
 </screen>
-  To ensure that this file system is automatically mounted at boot time as
+  The LUKS volume should be automatically picked up by
+  <command>nixos-generate-config</command>, but you might want to verify that your
+  <filename>hardware-configuration.nix</filename> looks correct.
+
+  To manually ensure that the system is automatically mounted at boot time as
   <filename>/</filename>, add the following to
   <filename>configuration.nix</filename>:
 <programlisting>
diff --git a/nixos/doc/manual/configuration/modularity.xml b/nixos/doc/manual/configuration/modularity.xml
index 532a2c615e4..d6eee4e9d76 100644
--- a/nixos/doc/manual/configuration/modularity.xml
+++ b/nixos/doc/manual/configuration/modularity.xml
@@ -133,7 +133,7 @@ true
 <programlisting>
 { config, pkgs, ... }:
 
-let netConfig = { hostName }: {
+let netConfig = hostName: {
   networking.hostName = hostName;
   networking.useDHCP = false;
 };
diff --git a/nixos/doc/manual/configuration/x-windows.xml b/nixos/doc/manual/configuration/x-windows.xml
index dd879702d7d..757174c5263 100644
--- a/nixos/doc/manual/configuration/x-windows.xml
+++ b/nixos/doc/manual/configuration/x-windows.xml
@@ -150,7 +150,6 @@
 <xref linkend="opt-services.xserver.videoDrivers"/> = [ "nvidiaLegacy390" ];
 <xref linkend="opt-services.xserver.videoDrivers"/> = [ "nvidiaLegacy340" ];
 <xref linkend="opt-services.xserver.videoDrivers"/> = [ "nvidiaLegacy304" ];
-<xref linkend="opt-services.xserver.videoDrivers"/> = [ "nvidiaLegacy173" ];
 </programlisting>
    You may need to reboot after enabling this driver to prevent a clash with
    other kernel modules.
@@ -159,21 +158,16 @@
  <simplesect xml:id="sec-x11--graphics-cards-amd">
   <title>Proprietary AMD drivers</title>
   <para>
-   AMD 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:
+   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:
 <programlisting>
-<xref linkend="opt-services.xserver.videoDrivers"/> = [ "ati_unfree" ];
+<xref linkend="opt-services.xserver.videoDrivers"/> = [ "amdgpu-pro" ];
 </programlisting>
    You will need to reboot after enabling this driver to prevent a clash with
    other kernel modules.
   </para>
-  <note>
-  <para>
-   For recent AMD GPUs you most likely want to keep either the defaults
-   or <literal>"amdgpu"</literal> (both free).
-  </para>
-  </note>
  </simplesect>
  <simplesect xml:id="sec-x11-touchpads">
   <title>Touchpads</title>
@@ -210,18 +204,18 @@
     XKB
    </link>
    keyboard layouts using the option
-   <option>
-    <link linkend="opt-services.xserver.extraLayouts">
-     services.xserver.extraLayouts
-    </link>
-   </option>.
+   <option><link linkend="opt-services.xserver.extraLayouts">
+     services.xserver.extraLayouts</link></option>.
+  </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>
-   To do this we are going to create a <literal>us-greek</literal> file
-   with a <literal>xkb_symbols</literal> section.
+   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>
 xkb_symbols &quot;us-greek&quot;
@@ -237,14 +231,13 @@ xkb_symbols &quot;us-greek&quot;
 };
 </programlisting>
   <para>
-   To install the layout, the filepath, a description and the list of
-   languages must be given:
+   A minimal layout specification must include the following:
   </para>
 <programlisting>
 <xref linkend="opt-services.xserver.extraLayouts"/>.us-greek = {
   description = "US layout with alt-gr greek";
   languages   = [ "eng" ];
-  symbolsFile = /path/to/us-greek;
+  symbolsFile = /yourpath/symbols/us-greek;
 }
 </programlisting>
   <note>
@@ -254,9 +247,27 @@ xkb_symbols &quot;us-greek&quot;
   </para>
   </note>
   <para>
-   The layout should now be installed and ready to use: try it by
-   running <literal>setxkbmap us-greek</literal> and type
-   <literal>&lt;alt&gt;+a</literal>. To change the default the usual
+   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>:
+<screen>
+<prompt>$ </prompt>nix-shell -p xorg.xkbcomp
+<prompt>$ </prompt>setxkbmap -I/yourpath us-greek -print | xkbcomp -I/yourpath - $DISPLAY
+</screen>
+  </para>
+  <para>
+   You can inspect the predefined XKB files for examples:
+<screen>
+<prompt>$ </prompt>echo "$(nix-build --no-out-link '&lt;nixpkgs&gt;' -A xorg.xkeyboardconfig)/etc/X11/xkb/"
+</screen>
+  </para>
+  <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
    <option>
     <link linkend="opt-services.xserver.layout">
      services.xserver.layout
diff --git a/nixos/doc/manual/development/settings-options.xml b/nixos/doc/manual/development/settings-options.xml
index c99c3af92f8..7292cac62b7 100644
--- a/nixos/doc/manual/development/settings-options.xml
+++ b/nixos/doc/manual/development/settings-options.xml
@@ -50,7 +50,7 @@
        </varlistentry>
        <varlistentry>
          <term>
-           <varname>pkgs.formats.ini</varname> { <replaceable>listsAsDuplicateKeys</replaceable> ? false, ... }
+           <varname>pkgs.formats.ini</varname> { <replaceable>listsAsDuplicateKeys</replaceable> ? false, <replaceable>listToValue</replaceable> ? null, ... }
          </term>
          <listitem>
            <para>
@@ -66,6 +66,16 @@
                    </para>
                  </listitem>
                </varlistentry>
+               <varlistentry>
+                 <term>
+                   <varname>listToValue</varname>
+                 </term>
+                 <listitem>
+                   <para>
+                     A function for turning a list of values into a single value.
+                   </para>
+                 </listitem>
+               </varlistentry>
              </variablelist>
             It returns a set with INI-specific attributes <varname>type</varname> and <varname>generate</varname> as specified <link linkend='pkgs-formats-result'>below</link>.
            </para>
@@ -167,7 +177,7 @@ in {
 
     # 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} = {};
+    users.users.${cfg.settings.user} = { isSystemUser = true; };
 
     # ...
   };
diff --git a/nixos/doc/manual/development/writing-documentation.xml b/nixos/doc/manual/development/writing-documentation.xml
index 32e00544cef..89fab666561 100644
--- a/nixos/doc/manual/development/writing-documentation.xml
+++ b/nixos/doc/manual/development/writing-documentation.xml
@@ -25,7 +25,8 @@
 
 <screen>
 <prompt>$ </prompt>cd /path/to/nixpkgs/nixos/doc/manual
-<prompt>$ </prompt>make
+<prompt>$ </prompt>nix-shell
+<prompt>nix-shell$ </prompt>make
 </screen>
 
   <para>
diff --git a/nixos/doc/manual/development/writing-modules.xml b/nixos/doc/manual/development/writing-modules.xml
index d244356dbed..fad4637f51f 100644
--- a/nixos/doc/manual/development/writing-modules.xml
+++ b/nixos/doc/manual/development/writing-modules.xml
@@ -74,7 +74,10 @@ linkend="sec-configuration-syntax"/>, we saw the following structure
    <callout arearefs='module-syntax-1'>
     <para>
      This line makes the current Nix expression a function. The variable
-     <varname>pkgs</varname> contains Nixpkgs, while <varname>config</varname>
+     <varname>pkgs</varname> contains Nixpkgs (by default, it takes the
+     <varname>nixpkgs</varname> entry of <envar>NIX_PATH</envar>, see the <link
+     xlink:href="https://nixos.org/manual/nix/stable/#sec-common-env">Nix
+     manual</link> for further details), while <varname>config</varname>
      contains the full system configuration. This line can be omitted if there
      is no reference to <varname>pkgs</varname> and <varname>config</varname>
      inside the module.
diff --git a/nixos/doc/manual/development/writing-nixos-tests.xml b/nixos/doc/manual/development/writing-nixos-tests.xml
index cab4c067e0d..5a95436915f 100644
--- a/nixos/doc/manual/development/writing-nixos-tests.xml
+++ b/nixos/doc/manual/development/writing-nixos-tests.xml
@@ -188,6 +188,25 @@ start_all()
    </varlistentry>
    <varlistentry>
     <term>
+     <methodname>get_screen_text_variants</methodname>
+    </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 <option>enableOCR</option> to the test attribute
+       set.
+      </para>
+     </note>
+    </listitem>
+   </varlistentry>
+   <varlistentry>
+    <term>
      <methodname>get_screen_text</methodname>
     </term>
     <listitem>
@@ -350,7 +369,8 @@ start_all()
      <para>
       Wait until the supplied regular expressions matches the textual contents
       of the screen by using optical character recognition (see
-      <methodname>get_screen_text</methodname>).
+     <methodname>get_screen_text</methodname> and
+     <methodname>get_screen_text_variants</methodname>).
      </para>
      <note>
       <para>
@@ -449,5 +469,16 @@ import ./make-test-python.nix {
     '';
 }
 </programlisting>
+  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):
+<programlisting>
+  testScript =
+    ''
+      # fmt: off
+      <replaceable>Python code…</replaceable>
+      # fmt: on
+    '';
+</programlisting>
  </para>
 </section>
diff --git a/nixos/doc/manual/installation/installing.xml b/nixos/doc/manual/installation/installing.xml
index bedeb7ccfa8..02f6bd6bed4 100644
--- a/nixos/doc/manual/installation/installing.xml
+++ b/nixos/doc/manual/installation/installing.xml
@@ -374,7 +374,7 @@
         You may want to look at the options starting with
         <option><link linkend="opt-boot.loader.efi.canTouchEfiVariables">boot.loader.efi</link></option>
         and
-        <option><link linkend="opt-boot.loader.systemd-boot.enable">boot.loader.systemd</link></option>
+        <option><link linkend="opt-boot.loader.systemd-boot.enable">boot.loader.systemd-boot</link></option>
         as well.
        </para>
       </listitem>
diff --git a/nixos/doc/manual/release-notes/rl-2009.xml b/nixos/doc/manual/release-notes/rl-2009.xml
index a6cff1a8fae..49446afdead 100644
--- a/nixos/doc/manual/release-notes/rl-2009.xml
+++ b/nixos/doc/manual/release-notes/rl-2009.xml
@@ -1019,6 +1019,14 @@ systemd.services.nginx.serviceConfig.ProtectHome = "read-only";
    </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 <varname>services.postgresql.checkConfig</varname> to
+        <literal>false</literal>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
       The rkt module has been removed, it was archived by upstream.
     </para>
    </listitem>
diff --git a/nixos/doc/manual/release-notes/rl-2105.xml b/nixos/doc/manual/release-notes/rl-2105.xml
index 302a6d3f374..bee1cddbecf 100644
--- a/nixos/doc/manual/release-notes/rl-2105.xml
+++ b/nixos/doc/manual/release-notes/rl-2105.xml
@@ -24,19 +24,80 @@
     </para>
    </listitem>
    <listitem>
-    <para>GNOME desktop environment was upgraded to 3.38, see its <link xlink:href="https://help.gnome.org/misc/release-notes/3.38/">release notes</link>.</para>
+    <para>The default Linux kernel was updated to the 5.10 LTS series, coming from the 5.4 LTS series.</para>
+   </listitem>
+   <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>.</para>
    </listitem>
    <listitem>
     <para>
      <link xlink:href="https://www.gnuradio.org/">GNURadio</link> 3.8 was
-     <link xlink:href="https://github.com/NixOS/nixpkgs/issues/82263">finnally</link>
+     <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 <code>gnuradio</code> and <code>gnuradio3_7</code>
      now point to an externally wrapped by default derivations, that allow you to
      also add `extraPythonPackages` to the Python interpreter used by GNURadio.
      Missing environmental variables needed for operational GUI were also added
-     (<link xlink:href="https://github.com/NixOS/nixpkgs/issues/75478">#7547</link>).
+     (<link xlink:href="https://github.com/NixOS/nixpkgs/issues/75478">#75478</link>).
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <link xlink:href="https://www.gnuradio.org/">GNURadio</link> has a
+     <code>pkgs</code> attribute set, and there's a <code>gnuradio.callPackage</code>
+     function that extends <code>pkgs</code> with a <code>mkDerivation</code>, and a
+     <code>mkDerivationWith</code>, like Qt5. Now all <code>gnuradio.pkgs</code> are
+     defined with <code>gnuradio.callPackage</code> 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 <option>services.privoxy.extraConfig</option> has been replaced by the new
+     <xref linkend="opt-services.privoxy.settings"/>
+     (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>
+     Python optimizations were disabled again. Builds with optimizations enabled
+     are not reproducible. Optimizations can now be enabled with an option.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <link xlink:href="https://kodi.tv/">Kodi</link> has been updated to version 19.0 "Matrix". See
+     the <link xlink:href="https://kodi.tv/article/kodi-190-matrix-release">announcement</link> for
+     further details.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <option>services.packagekit.backend</option> 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 <xref linkend="opt-services.packagekit.settings"/>
+     and <xref linkend="opt-services.packagekit.vendorSettings"/> 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>
   </itemizedlist>
@@ -74,6 +135,16 @@
        <xref linkend="opt-services.samba-wsdd.enable" /> 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>
   </itemizedlist>
 
  </section>
@@ -200,7 +271,7 @@
    </listitem>
    <listitem>
      <para>
-       xfsprogs was update from 4.19 to 5.10. It now enables reflink support by default on filesystem creation.
+       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>.
@@ -259,7 +330,18 @@
    </listitem>
    <listitem>
     <para>
-      <literal>vim</literal> switched to Python 3, dropping all Python 2 support.
+      <literal>vim</literal> and <literal>neovim</literal> switched to Python 3, dropping all Python 2 support.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <link linkend="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>
@@ -523,6 +605,115 @@ self: super:
       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 <xref linkend="opt-services.gitlab.smtp.port" />
+      option.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The default version of ImageMagick has been updated from 6 to 7.
+     You can use <package>imagemagick6</package>,
+     <package>imagemagick6_light</package>, and
+     <package>imagemagick6Big</package> if you need the older version.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <xref linkend="opt-services.xserver.videoDrivers" /> 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 <package>apacheKafka</package> 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 linkend="opt-services.apache-kafka.jre">services.apache-kafka.jre</link>
+      option. Furthermore, the default list of
+      <link linkend="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 <package>kodi</package> package has been modified to allow concise addon management. Consider
+     the following configuration from previous releases of NixOS to install <package>kodi</package>,
+     including the <package>kodiPackages.inputstream-adaptive</package> and <package>kodiPackages.vfs-sftp</package>
+     addons:
+
+     <programlisting>
+environment.systemPackages = [
+  pkgs.kodi
+];
+
+nixpkgs.config.kodi = {
+  enableInputStreamAdaptive = true;
+  enableVFSSFTP = true;
+};
+     </programlisting>
+
+     All Kodi <literal>config</literal> flags have been removed, and as a result the above configuration
+     should now be written as:
+
+     <programlisting>
+environment.systemPackages = [
+  (pkgs.kodi.withPackages (p: with p; [
+    inputstream-adaptive
+    vfs-sftp
+  ]))
+];
+     </programlisting>
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <option>environment.defaultPackages</option> now includes the nano package.
+     If <package>pkgs.nano</package> 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 <option>environment.variables</option>.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <option>services.minio.dataDir</option> 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 <package>babeld</package> 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 <varname>services.babeld.interfaces</varname>.
+     </para>
+    </listitem>
+   <listitem>
+     <para>
+      The <option>services.zigbee2mqtt.config</option> option has been renamed to <option>services.zigbee2mqtt.settings</option> and
+      now follows <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">RFC 0042</link>.
+     </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <package>yadm</package> 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>
    </listitem>
   </itemizedlist>
  </section>
@@ -558,14 +749,16 @@ self: super:
    </listitem>
    <listitem>
     <para>
-     The default-version of <literal>nextcloud</literal> is <package>nextcloud20</package>.
+     The default-version of <literal>nextcloud</literal> is <package>nextcloud21</package>.
      Please note that it's <emphasis>not</emphasis> possible to upgrade <literal>nextcloud</literal>
      across multiple major versions! This means that it's e.g. not possible to upgrade
-     from <package>nextcloud18</package> to <package>nextcloud20</package> in a single deploy.
+     from <package>nextcloud18</package> to <package>nextcloud20</package> in a single deploy and
+     most <literal>20.09</literal> users will have to upgrade to <package>nextcloud20</package>
+     first.
     </para>
     <para>
      The package can be manually upgraded by setting <xref linkend="opt-services.nextcloud.package" />
-     to <package>nextcloud20</package>.
+     to <package>nextcloud21</package>.
     </para>
    </listitem>
    <listitem>
@@ -617,8 +810,8 @@ self: super:
     </para>
 
     <para>
-     Aditionally to the much stricter runtime environmet the
-     <literal>/dev/urandom</literal> mount lines we previously had in the code (that would
+     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>
 
@@ -636,6 +829,23 @@ self: super:
      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,
+     <option>sevices.unbound.extraConfig</option> has been removed and replaced
+     by <xref linkend="opt-services.unbound.settings"/>. <option>services.unbound.interfaces</option>
+     has been renamed to <option>services.unbound.settings.server.interface</option>.
+    </para>
+
+    <para>
+     <option>services.unbound.forwardAddresses</option> and
+     <option>services.unbound.allowedAccess</option> 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>
@@ -693,6 +903,13 @@ self: super:
     </para>
    </listitem>
    <listitem>
+    <para>
+      When defining a new user, one of <xref linkend="opt-users.users._name_.isNormalUser" /> and <xref linkend="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 <xref linkend="opt-users.users._name_.isSystemUser" /> has no effect for those.
+    </para>
+   </listitem>
+   <listitem>
      <para>
        The GNOME desktop manager once again installs <package>gnome3.epiphany</package> by default.
      </para>
@@ -706,7 +923,7 @@ self: super:
    <listitem>
     <para>
      Platforms, like <varname>stdenv.hostPlatform</varname>, no longer have a <varname>platform</varname> attribute.
-     It has been (mostly) flattoned away:
+     It has been (mostly) flattened away:
     </para>
     <itemizedlist>
      <listitem><para><varname>platform.gcc</varname> is now <varname>gcc</varname></para></listitem>
@@ -730,6 +947,94 @@ self: super:
      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:
+     <package>cargoSetupHook</package> to set up vendoring for Cargo-based projects,
+     <package>cargoBuildHook</package> to build a project using Cargo,
+     <package>cargoInstallHook</package> to install a project using Cargo, and
+     <package>cargoCheckHook</package> 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:
+
+     <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>
+    </para>
+   </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 linkend="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 linkend="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:
+     <programlisting>
+<xref linkend="opt-services.gvfs.package" /> = pkgs.gvfs.override { samba = null; };
+     </programlisting>
+    </para>
+   </listitem>
   </itemizedlist>
  </section>
 </section>
diff --git a/nixos/lib/make-disk-image.nix b/nixos/lib/make-disk-image.nix
index 023d0791a5c..7d40d3b5548 100644
--- a/nixos/lib/make-disk-image.nix
+++ b/nixos/lib/make-disk-image.nix
@@ -15,6 +15,8 @@
 
 , # 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.
@@ -163,6 +165,8 @@ let format' = format; in let
 
   closureInfo = pkgs.closureInfo { rootPaths = [ config.system.build.toplevel channelSources ]; };
 
+  blockSize = toString (4 * 1024); # ext4fs block size (not block device sector size)
+
   prepareImage = ''
     export PATH=${binPath}
 
@@ -175,6 +179,24 @@ let format' = format; in let
       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"
@@ -235,12 +257,53 @@ let format' = format; in let
 
     ${if diskSize == "auto" then ''
       ${if partitionTableType == "efi" || partitionTableType == "hybrid" then ''
-        additionalSpace=$(( ($(numfmt --from=iec '${additionalSpace}') + $(numfmt --from=iec '${bootSize}')) / 1000 ))
+        # 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 ''
-        additionalSpace=$(( $(numfmt --from=iec '${additionalSpace}') / 1000 ))
+        reservedSpace=0
       ''}
-      diskSize=$(( $(set -- $(du -d0 $root); echo "$1") + $additionalSpace ))
-      truncate -s "$diskSize"K $diskImage
+      additionalSpace=$(( $(numfmt --from=iec '${additionalSpace}') + reservedSpace ))
+
+      # Compute required space in filesystem blocks
+      diskUsage=$(find . ! -type d -exec 'du' '--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
     ''}
@@ -251,9 +314,9 @@ let format' = format; in let
       # Get start & length of the root partition in sectors to $START and $SECTORS.
       eval $(partx $diskImage -o START,SECTORS --nr ${rootPartition} --pairs)
 
-      mkfs.${fsType} -F -L ${label} $diskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
+      mkfs.${fsType} -b ${blockSize} -F -L ${label} $diskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
     '' else ''
-      mkfs.${fsType} -F -L ${label} $diskImage
+      mkfs.${fsType} -b ${blockSize} -F -L ${label} $diskImage
     ''}
 
     echo "copying staging root to image..."
@@ -283,6 +346,9 @@ in pkgs.vmTools.runInLinuxVM (
       # 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
diff --git a/nixos/lib/make-options-doc/default.nix b/nixos/lib/make-options-doc/default.nix
index a1161621f0d..14015ab64ab 100644
--- a/nixos/lib/make-options-doc/default.nix
+++ b/nixos/lib/make-options-doc/default.nix
@@ -126,11 +126,37 @@ let
     }
   '';
 
+  singleMDDoc = name: value: ''
+    ## ${lib.escape [ "<" ">" ] name}
+    ${value.description}
+
+    ${lib.optionalString (value ? type) ''
+      *_Type_*:
+      ${value.type}
+    ''}
+
+    ${lib.optionalString (value ? default) ''
+      *_Default_*
+      ```
+      ${builtins.toJSON value.default}
+      ```
+    ''}
+
+    ${lib.optionalString (value ? example) ''
+      *_Example_*
+      ```
+      ${builtins.toJSON value.example}
+      ```
+    ''}
+  '';
+
 in {
   inherit optionsNix;
 
   optionsAsciiDoc = lib.concatStringsSep "\n" (lib.mapAttrsToList singleAsciiDoc optionsNix);
 
+  optionsMDDoc = lib.concatStringsSep "\n" (lib.mapAttrsToList singleMDDoc optionsNix);
+
   optionsJSON = pkgs.runCommand "options.json"
     { meta.description = "List of NixOS options in JSON format";
       buildInputs = [ pkgs.brotli ];
diff --git a/nixos/lib/make-squashfs.nix b/nixos/lib/make-squashfs.nix
index ee76c9c5bf2..8690c42e7ac 100644
--- a/nixos/lib/make-squashfs.nix
+++ b/nixos/lib/make-squashfs.nix
@@ -23,6 +23,6 @@ stdenv.mkDerivation {
 
       # Generate the squashfs image.
       mksquashfs nix-path-registration $(cat $closureInfo/store-paths) $out \
-        -keep-as-directory -all-root -b 1048576 -comp ${comp}
+        -no-hardlinks -keep-as-directory -all-root -b 1048576 -comp ${comp}
     '';
 }
diff --git a/nixos/lib/qemu-flags.nix b/nixos/lib/qemu-flags.nix
index 0f066245893..f786745ba32 100644
--- a/nixos/lib/qemu-flags.nix
+++ b/nixos/lib/qemu-flags.nix
@@ -18,13 +18,15 @@ rec {
     ];
 
   qemuSerialDevice = if pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64 then "ttyS0"
-        else if pkgs.stdenv.isAarch32 || pkgs.stdenv.isAarch64 then "ttyAMA0"
+        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/test-driver/test-driver.py b/nixos/lib/test-driver/test-driver.py
index 96b75a49928..270a5969cda 100644
--- a/nixos/lib/test-driver/test-driver.py
+++ b/nixos/lib/test-driver/test-driver.py
@@ -1,7 +1,7 @@
 #! /somewhere/python3
 from contextlib import contextmanager, _GeneratorContextManager
 from queue import Queue, Empty
-from typing import Tuple, Any, Callable, Dict, Iterator, Optional, List
+from typing import Tuple, Any, Callable, Dict, Iterator, Optional, List, Iterable
 from xml.sax.saxutils import XMLGenerator
 import queue
 import io
@@ -205,6 +205,37 @@ class Logger:
         self.xml.endElement("nest")
 
 
+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
+
+
 class Machine:
     def __init__(self, args: Dict[str, Any]) -> None:
         if "name" in args:
@@ -637,43 +668,29 @@ class Machine:
         """Debugging: Dump the contents of the TTY<n>"""
         self.execute("fold -w 80 /dev/vcs{} | systemd-cat".format(tty))
 
-    def get_screen_text(self) -> str:
-        if shutil.which("tesseract") is None:
-            raise Exception("get_screen_text used 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 = "-c debug_file=/dev/null --psm 11 --oem 2"
+    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)
 
-        with self.nested("performing optical character recognition"):
-            with tempfile.NamedTemporaryFile() as tmpin:
-                self.send_monitor_command("screendump {}".format(tmpin.name))
-
-                cmd = "convert {} {} tiff:- | tesseract - - {}".format(
-                    magick_args, tmpin.name, tess_args
-                )
-                ret = subprocess.run(cmd, shell=True, capture_output=True)
-                if ret.returncode != 0:
-                    raise Exception(
-                        "OCR failed with exit code {}".format(ret.returncode)
-                    )
+    def get_screen_text_variants(self) -> List[str]:
+        return self._get_screen_text_variants([0, 1, 2])
 
-                return ret.stdout.decode("utf-8")
+    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:
-            text = self.get_screen_text()
-            matches = re.search(regex, text) is not None
+            variants = self.get_screen_text_variants()
+            for text in variants:
+                if re.search(regex, text) is not None:
+                    return True
 
-            if last and not matches:
-                self.log("Last OCR attempt failed. Text was: {}".format(text))
+            if last:
+                self.log("Last OCR attempt failed. Text was: {}".format(variants))
 
-            return matches
+            return False
 
         with self.nested("waiting for {} to appear on screen".format(regex)):
             retry(screen_matches)
@@ -718,6 +735,7 @@ class Machine:
         shell_path = os.path.join(self.state_dir, "shell")
         self.shell_socket = create_socket(shell_path)
 
+        display_available = any(x in os.environ for x in ["DISPLAY", "WAYLAND_DISPLAY"])
         qemu_options = (
             " ".join(
                 [
@@ -727,7 +745,7 @@ class Machine:
                     "-device virtio-serial",
                     "-device virtconsole,chardev=shell",
                     "-device virtio-rng-pci",
-                    "-serial stdio" if "DISPLAY" in os.environ else "-nographic",
+                    "-serial stdio" if display_available else "-nographic",
                 ]
             )
             + " "
diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix
index 6192be1cd05..6497b897eaf 100644
--- a/nixos/lib/testing-python.nix
+++ b/nixos/lib/testing-python.nix
@@ -54,8 +54,13 @@ rec {
     };
 
   # Run an automated test suite in the given virtual network.
-  # `driver' is the script that runs the network.
-  runTests = driver:
+  runTests = {
+    # the script that runs the network
+    driver,
+    # a source position in the format of builtins.unsafeGetAttrPos
+    # for meta.position
+    pos,
+  }:
     stdenv.mkDerivation {
       name = "vm-test-run-${driver.testName}";
 
@@ -69,6 +74,8 @@ rec {
         '';
 
       passthru = driver.passthru;
+
+      inherit pos;
     };
 
 
@@ -79,6 +86,11 @@ rec {
       # 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
@@ -131,10 +143,8 @@ rec {
                   "it's currently ${toString testNameLen} characters long.")
             else
               "nixos-test-driver-${name}";
-
-          warn = if skipLint then lib.warn "Linting is disabled!" else lib.id;
         in
-        warn (runCommand testDriverName
+        lib.warnIf skipLint "Linting is disabled" (runCommand testDriverName
           {
             buildInputs = [ makeWrapper ];
             testScript = testScript';
@@ -176,7 +186,7 @@ rec {
       driver = mkDriver null;
       driverInteractive = mkDriver pkgs.qemu;
 
-      test = passMeta (runTests driver);
+      test = passMeta (runTests { inherit driver pos; });
 
       nodeNames = builtins.attrNames driver.nodes;
       invalidNodeNames = lib.filter
diff --git a/nixos/maintainers/scripts/cloudstack/cloudstack-image.nix b/nixos/maintainers/scripts/cloudstack/cloudstack-image.nix
index 37b46db059c..005f75476e9 100644
--- a/nixos/maintainers/scripts/cloudstack/cloudstack-image.nix
+++ b/nixos/maintainers/scripts/cloudstack/cloudstack-image.nix
@@ -10,7 +10,6 @@ with lib;
 
   system.build.cloudstackImage = import ../../../lib/make-disk-image.nix {
     inherit lib config pkgs;
-    diskSize = 8192;
     format = "qcow2";
     configFile = pkgs.writeText "configuration.nix"
       ''
diff --git a/nixos/maintainers/scripts/ec2/amazon-image.nix b/nixos/maintainers/scripts/ec2/amazon-image.nix
index 0ecf07669a1..677aff4421e 100644
--- a/nixos/maintainers/scripts/ec2/amazon-image.nix
+++ b/nixos/maintainers/scripts/ec2/amazon-image.nix
@@ -40,8 +40,9 @@ in {
     };
 
     sizeMB = mkOption {
-      type = types.int;
+      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";
     };
 
diff --git a/nixos/maintainers/scripts/gce/create-gce.sh b/nixos/maintainers/scripts/gce/create-gce.sh
index 77cc64e591e..0eec4d04110 100755
--- a/nixos/maintainers/scripts/gce/create-gce.sh
+++ b/nixos/maintainers/scripts/gce/create-gce.sh
@@ -17,7 +17,19 @@ nix-build '<nixpkgs/nixos/lib/eval-config.nix>' \
 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/openstack/openstack-image.nix b/nixos/maintainers/scripts/openstack/openstack-image.nix
index 4c464f43f61..3255e7f3d44 100644
--- a/nixos/maintainers/scripts/openstack/openstack-image.nix
+++ b/nixos/maintainers/scripts/openstack/openstack-image.nix
@@ -12,8 +12,8 @@ with lib;
 
   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
-    diskSize = 8192;
     format = "qcow2";
     configFile = pkgs.writeText "configuration.nix"
       ''
diff --git a/nixos/modules/config/iproute2.nix b/nixos/modules/config/iproute2.nix
index a1d9ebcec66..5f41f3d21e4 100644
--- a/nixos/modules/config/iproute2.nix
+++ b/nixos/modules/config/iproute2.nix
@@ -18,15 +18,15 @@ in
   };
 
   config = mkIf cfg.enable {
-    environment.etc."iproute2/bpf_pinning" = { mode = "0644"; text = fileContents "${pkgs.iproute}/etc/iproute2/bpf_pinning"; };
-    environment.etc."iproute2/ematch_map"  = { mode = "0644"; text = fileContents "${pkgs.iproute}/etc/iproute2/ematch_map";  };
-    environment.etc."iproute2/group"       = { mode = "0644"; text = fileContents "${pkgs.iproute}/etc/iproute2/group";       };
-    environment.etc."iproute2/nl_protos"   = { mode = "0644"; text = fileContents "${pkgs.iproute}/etc/iproute2/nl_protos";   };
-    environment.etc."iproute2/rt_dsfield"  = { mode = "0644"; text = fileContents "${pkgs.iproute}/etc/iproute2/rt_dsfield";  };
-    environment.etc."iproute2/rt_protos"   = { mode = "0644"; text = fileContents "${pkgs.iproute}/etc/iproute2/rt_protos";   };
-    environment.etc."iproute2/rt_realms"   = { mode = "0644"; text = fileContents "${pkgs.iproute}/etc/iproute2/rt_realms";   };
-    environment.etc."iproute2/rt_scopes"   = { mode = "0644"; text = fileContents "${pkgs.iproute}/etc/iproute2/rt_scopes";   };
-    environment.etc."iproute2/rt_tables"   = { mode = "0644"; text = (fileContents "${pkgs.iproute}/etc/iproute2/rt_tables")
+    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/pulseaudio.nix b/nixos/modules/config/pulseaudio.nix
index c0e90a8c26e..0266bbfb121 100644
--- a/nixos/modules/config/pulseaudio.nix
+++ b/nixos/modules/config/pulseaudio.nix
@@ -306,6 +306,7 @@ in {
         description = "PulseAudio system service user";
         home = stateDir;
         createHome = true;
+        isSystemUser = true;
       };
 
       users.groups.pulse.gid = gid;
diff --git a/nixos/modules/config/system-path.nix b/nixos/modules/config/system-path.nix
index aee7a041d04..1292c3008c6 100644
--- a/nixos/modules/config/system-path.nix
+++ b/nixos/modules/config/system-path.nix
@@ -29,7 +29,6 @@ let
       pkgs.xz
       pkgs.less
       pkgs.libcap
-      pkgs.nano
       pkgs.ncurses
       pkgs.netcat
       config.programs.ssh.package
@@ -43,7 +42,8 @@ let
     ];
 
     defaultPackages = map (pkg: setPrio ((pkg.meta.priority or 5) + 3) pkg)
-      [ pkgs.perl
+      [ pkgs.nano
+        pkgs.perl
         pkgs.rsync
         pkgs.strace
       ];
@@ -75,13 +75,21 @@ in
         default = defaultPackages;
         example = literalExample "[]";
         description = ''
-          Set of packages users expect from a minimal linux istall.
-          Like systemPackages, they appear in
-          /run/current-system/sw.  These packages are
+          Set of default packages that aren't strictly neccessary
+          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.
-          If you want a more minimal system, set it to an empty list.
         '';
       };
 
diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl
index 44040217b02..bef08dc4020 100644
--- a/nixos/modules/config/update-users-groups.pl
+++ b/nixos/modules/config/update-users-groups.pl
@@ -288,7 +288,7 @@ foreach my $u (values %usersOut) {
     push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::::") . "\n";
 }
 
-updateFile("/etc/shadow", \@shadowNew, 0600);
+updateFile("/etc/shadow", \@shadowNew, 0640);
 {
     my $uid = getpwnam "root";
     my $gid = getgrnam "shadow";
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
index 1a530b9f013..567a8b6f3b9 100644
--- a/nixos/modules/config/users-groups.nix
+++ b/nixos/modules/config/users-groups.nix
@@ -6,6 +6,12 @@ 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
@@ -54,7 +60,7 @@ let
     options = {
 
       name = mkOption {
-        type = types.str;
+        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
@@ -63,7 +69,7 @@ let
       };
 
       description = mkOption {
-        type = types.str;
+        type = passwdEntry types.str;
         default = "";
         example = "Alice Q. User";
         description = ''
@@ -92,6 +98,8 @@ let
           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.
         '';
       };
 
@@ -107,6 +115,8 @@ let
           <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.
         '';
       };
 
@@ -124,7 +134,7 @@ let
       };
 
       home = mkOption {
-        type = types.path;
+        type = passwdEntry types.path;
         default = "/var/empty";
         description = "The user's home directory.";
       };
@@ -153,7 +163,7 @@ let
       };
 
       shell = mkOption {
-        type = types.nullOr (types.either types.shellPackage types.path);
+        type = types.nullOr (types.either types.shellPackage (passwdEntry types.path));
         default = pkgs.shadow;
         defaultText = "pkgs.shadow";
         example = literalExample "pkgs.bashInteractive";
@@ -213,7 +223,7 @@ let
       };
 
       hashedPassword = mkOption {
-        type = with types; nullOr str;
+        type = with types; nullOr (passwdEntry str);
         default = null;
         description = ''
           Specifies the hashed password for the user.
@@ -247,7 +257,7 @@ let
       };
 
       initialHashedPassword = mkOption {
-        type = with types; nullOr str;
+        type = with types; nullOr (passwdEntry str);
         default = null;
         description = ''
           Specifies the initial hashed password for the user, i.e. the
@@ -319,7 +329,7 @@ let
     options = {
 
       name = mkOption {
-        type = types.str;
+        type = passwdEntry types.str;
         description = ''
           The name of the group. If undefined, the name of the attribute set
           will be used.
@@ -336,7 +346,7 @@ let
       };
 
       members = mkOption {
-        type = with types; listOf str;
+        type = with types; listOf (passwdEntry str);
         default = [];
         description = ''
           The user names of the group members, added to the
@@ -521,6 +531,7 @@ in {
       };
       nobody = {
         uid = ids.uids.nobody;
+        isSystemUser = true;
         description = "Unprivileged account (don't use!)";
         group = "nogroup";
       };
@@ -556,10 +567,8 @@ in {
         install -m 0700 -d /root
         install -m 0755 -d /home
 
-        ${pkgs.perl}/bin/perl -w \
-          -I${pkgs.perlPackages.FileSlurp}/${pkgs.perl.libPrefix} \
-          -I${pkgs.perlPackages.JSON}/${pkgs.perl.libPrefix} \
-          ${./update-users-groups.pl} ${spec}
+        ${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \
+        -w ${./update-users-groups.pl} ${spec}
       '';
 
     # for backwards compatibility
@@ -593,8 +602,8 @@ in {
         # password or an SSH authorized key. Privileged accounts are
         # root and users in the wheel group.
         assertion = !cfg.mutableUsers ->
-          any id ((mapAttrsToList (name: cfg:
-            (name == "root"
+          any id ((mapAttrsToList (_: cfg:
+            (cfg.name == "root"
              || cfg.group == "wheel"
              || elem "wheel" cfg.extraGroups)
             &&
@@ -610,21 +619,32 @@ in {
           Neither the root account nor any wheel user has a password or SSH authorized key.
           You must set one to prevent being locked out of your system.'';
       }
-    ] ++ flip mapAttrsToList cfg.users (name: user:
-      {
+    ] ++ flatten (flip mapAttrsToList cfg.users (name: user:
+      [
+        {
         assertion = (user.hashedPassword != null)
-                    -> (builtins.match ".*:.*" user.hashedPassword == null);
+        -> (builtins.match ".*:.*" user.hashedPassword == null);
         message = ''
-          The password hash of user "${name}" contains a ":" character.
-          This is invalid and would break the login system because the fields
-          of /etc/shadow (file where hashes are stored) are colon-separated.
-          Please check the value of option `users.users."${name}".hashedPassword`.'';
-      }
-    );
+            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.
+            '';
+          }
+        ]
+    ));
 
     warnings =
       builtins.filter (x: x != null) (
-        flip mapAttrsToList cfg.users (name: user:
+        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)
@@ -647,9 +667,9 @@ in {
             && user.hashedPassword != ""  # login without password
             && builtins.match mcf user.hashedPassword == null)
         then ''
-          The password hash of user "${name}" may be invalid. You must set a
+          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."${name}".hashedPassword`.''
+          check the value of option `users.users."${user.name}".hashedPassword`.''
         else null
       ));
 
diff --git a/nixos/modules/hardware/all-firmware.nix b/nixos/modules/hardware/all-firmware.nix
index 8cf3e5633dc..3e88a4c20ad 100644
--- a/nixos/modules/hardware/all-firmware.nix
+++ b/nixos/modules/hardware/all-firmware.nix
@@ -49,7 +49,7 @@ in {
         rt5677-firmware
         rtl8723bs-firmware
         rtl8761b-firmware
-        rtlwifi_new-firmware
+        rtw88-firmware
         zd1211fw
         alsa-firmware
         sof-firmware
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/printers.nix b/nixos/modules/hardware/printers.nix
index 752de41f26d..c587076dcd1 100644
--- a/nixos/modules/hardware/printers.nix
+++ b/nixos/modules/hardware/printers.nix
@@ -15,7 +15,7 @@ let
       ${ppdOptionsString p.ppdOptions}
   '';
   ensureDefaultPrinter = name: ''
-    ${pkgs.cups}/bin/lpoptions -d '${name}'
+    ${pkgs.cups}/bin/lpadmin -d '${name}'
   '';
 
   # "graph but not # or /" can't be implemented as regex alone due to missing lookahead support
diff --git a/nixos/modules/hardware/rtl-sdr.nix b/nixos/modules/hardware/rtl-sdr.nix
index 77c8cb59a3d..9605c7967f6 100644
--- a/nixos/modules/hardware/rtl-sdr.nix
+++ b/nixos/modules/hardware/rtl-sdr.nix
@@ -6,14 +6,13 @@ let
 in {
   options.hardware.rtl-sdr = {
     enable = lib.mkEnableOption ''
-      Enables rtl-sdr udev rules and ensures 'plugdev' group exists.
-      This is a prerequisite to using devices supported by rtl-sdr without
-      being root, since rtl-sdr USB descriptors will be owned by plugdev
-      through udev.
+      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..541897527a8
--- /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/system-76.nix b/nixos/modules/hardware/system-76.nix
index 48eb63f4f22..ed661fd3303 100644
--- a/nixos/modules/hardware/system-76.nix
+++ b/nixos/modules/hardware/system-76.nix
@@ -17,6 +17,9 @@ let
 
   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 = {
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/video/amdgpu.nix b/nixos/modules/hardware/video/amdgpu.nix
deleted file mode 100644
index 42fc8fa362d..00000000000
--- a/nixos/modules/hardware/video/amdgpu.nix
+++ /dev/null
@@ -1,9 +0,0 @@
-{ config, lib, ... }:
-
-with lib;
-{
-  config = mkIf (elem "amdgpu" config.services.xserver.videoDrivers) {
-    boot.blacklistedKernelModules = [ "radeon" ];
-  };
-}
-
diff --git a/nixos/modules/i18n/input-method/default.nix b/nixos/modules/i18n/input-method/default.nix
index 4649f9b862a..bbc5783565a 100644
--- a/nixos/modules/i18n/input-method/default.nix
+++ b/nixos/modules/i18n/input-method/default.nix
@@ -29,7 +29,7 @@ in
   options.i18n = {
     inputMethod = {
       enabled = mkOption {
-        type    = types.nullOr (types.enum [ "ibus" "fcitx" "fcitx5" "nabi" "uim" "hime" ]);
+        type    = types.nullOr (types.enum [ "ibus" "fcitx" "fcitx5" "nabi" "uim" "hime" "kime" ]);
         default = null;
         example = "fcitx";
         description = ''
@@ -46,6 +46,7 @@ in
           <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>
         '';
       };
diff --git a/nixos/modules/i18n/input-method/default.xml b/nixos/modules/i18n/input-method/default.xml
index 73911059f8a..dd66316c730 100644
--- a/nixos/modules/i18n/input-method/default.xml
+++ b/nixos/modules/i18n/input-method/default.xml
@@ -40,6 +40,11 @@
     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>
@@ -266,4 +271,21 @@ i18n.inputMethod = {
 };
 </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/kime.nix b/nixos/modules/i18n/input-method/kime.nix
new file mode 100644
index 00000000000..2a73cb3f460
--- /dev/null
+++ b/nixos/modules/i18n/input-method/kime.nix
@@ -0,0 +1,49 @@
+{ 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 = literalExample ''
+          {
+            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);
+  };
+}
+
diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index 1418420afcd..7a4738599b0 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -162,12 +162,14 @@ let
   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 targetArch == "x64" then
+    if refindBinary != null then
       ''
       # Adds rEFInd to the ISO.
-      cp -v ${pkgs.refind}/share/refind/refind_x64.efi $out/EFI/boot/
+      cp -v ${pkgs.refind}/share/refind/${refindBinary} $out/EFI/boot/
       ''
     else
       "# No refind for ${targetArch}"
@@ -186,7 +188,10 @@ let
 
     # Fonts can be loaded?
     # (This font is assumed to always be provided as a fallback by NixOS)
-    if loadfont (hd0)/EFI/boot/unicode.pf2; then
+    if loadfont /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.
@@ -207,11 +212,11 @@ let
     ${ # When there is a theme configured, use it, otherwise use the background image.
     if config.isoImage.grubTheme != null then ''
       # Sets theme.
-      set theme=(hd0)/EFI/boot/grub-theme/theme.txt
+      set theme=/EFI/boot/grub-theme/theme.txt
       # Load theme fonts
-      $(find ${config.isoImage.grubTheme} -iname '*.pf2' -printf "loadfont (hd0)/EFI/boot/grub-theme/%P\n")
+      $(find ${config.isoImage.grubTheme} -iname '*.pf2' -printf "loadfont /EFI/boot/grub-theme/%P\n")
     '' else ''
-      if background_image (hd0)/EFI/boot/efi-background.png; then
+      if background_image /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
@@ -264,6 +269,8 @@ let
 
     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
@@ -273,8 +280,28 @@ let
     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 /EFI/boot/unicode.pf2
+      set textmode=true
+      terminal_output gfxterm console
+    }
+    hiddenentry 'GUI mode' --hotkey 'g' {
+      $(find ${config.isoImage.grubTheme} -iname '*.pf2' -printf "loadfont /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
@@ -337,11 +364,15 @@ let
       }
     }
 
-    menuentry 'rEFInd' --class refind {
-      # UUID is hard-coded in the derivation.
-      search --set=root --no-floppy --fs-uuid 1234-5678
-      chainloader (\$root)/EFI/boot/refind_x64.efi
-    }
+    ${lib.optionalString (refindBinary != null) ''
+    # GRUB apparently cannot do "chainloader" operations on "CD".
+    if [ "\$root" != "cd0" ]; then
+      menuentry 'rEFInd' --class refind {
+        # \$root defaults to the drive the EFI is found on.
+        chainloader (\$root)/EFI/boot/${refindBinary}
+      }
+    fi
+    ''}
     menuentry 'Firmware Setup' --class settings {
       fwsetup
       clear
diff --git a/nixos/modules/installer/sd-card/sd-image-aarch64.nix b/nixos/modules/installer/sd-card/sd-image-aarch64.nix
index 96ebb7537da..165e2aac27b 100644
--- a/nixos/modules/installer/sd-card/sd-image-aarch64.nix
+++ b/nixos/modules/installer/sd-card/sd-image-aarch64.nix
@@ -18,13 +18,6 @@
   # - ttyAMA0: for QEMU's -machine virt
   boot.kernelParams = ["console=ttyS0,115200n8" "console=ttyAMA0,115200n8" "console=tty0"];
 
-  boot.initrd.availableKernelModules = [
-    # Allows early (earlier) modesetting for the Raspberry Pi
-    "vc4" "bcm2835_dma" "i2c_bcm2835"
-    # Allows early (earlier) modesetting for Allwinner SoCs
-    "sun4i_drm" "sun8i_drm_hdmi" "sun8i_mixer"
-  ];
-
   sdImage = {
     populateFirmwareCommands = let
       configTxt = pkgs.writeText "config.txt" ''
diff --git a/nixos/modules/installer/sd-card/sd-image.nix b/nixos/modules/installer/sd-card/sd-image.nix
index b811ae07eb0..d0fe79903d3 100644
--- a/nixos/modules/installer/sd-card/sd-image.nix
+++ b/nixos/modules/installer/sd-card/sd-image.nix
@@ -29,6 +29,7 @@ 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 = {
@@ -126,6 +127,13 @@ in
       '';
     };
 
+    expandOnBoot = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to configure the sd image to expand it's partition on boot.
+      '';
+    };
   };
 
   config = {
@@ -215,7 +223,7 @@ in
       '';
     }) {};
 
-    boot.postBootCommands = ''
+    boot.postBootCommands = lib.mkIf config.sdImage.expandOnBoot ''
       # On the first boot do some maintenance tasks
       if [ -f /nix-path-registration ]; then
         set -euo pipefail
diff --git a/nixos/modules/installer/tools/nixos-install.sh b/nixos/modules/installer/tools/nixos-install.sh
index 9d49d4055e4..ea9667995e1 100644
--- a/nixos/modules/installer/tools/nixos-install.sh
+++ b/nixos/modules/installer/tools/nixos-install.sh
@@ -125,7 +125,7 @@ fi
 
 # Resolve the flake.
 if [[ -n $flake ]]; then
-    flake=$(nix "${flakeFlags[@]}" flake info --json "${extraBuildFlags[@]}" "${lockFlags[@]}" -- "$flake" | jq -r .url)
+    flake=$(nix "${flakeFlags[@]}" flake metadata --json "${extraBuildFlags[@]}" "${lockFlags[@]}" -- "$flake" | jq -r .url)
 fi
 
 if [[ ! -e $NIXOS_CONFIG && -z $system && -z $flake ]]; then
diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix
index a9e5641b05a..cb2dbf6c859 100644
--- a/nixos/modules/installer/tools/tools.nix
+++ b/nixos/modules/installer/tools/tools.nix
@@ -34,7 +34,7 @@ let
     name = "nixos-generate-config";
     src = ./nixos-generate-config.pl;
     path = lib.optionals (lib.elem "btrfs" config.boot.supportedFilesystems) [ pkgs.btrfs-progs ];
-    perl = "${pkgs.perl}/bin/perl -I${pkgs.perlPackages.FileSlurp}/${pkgs.perl.libPrefix}";
+    perl = "${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl";
     inherit (config.system.nixos-generate-config) configuration desktopConfiguration;
     xserverEnabled = config.services.xserver.enable;
   };
@@ -163,7 +163,8 @@ in
         # List packages installed in system profile. To search, run:
         # \$ nix search wget
         # environment.systemPackages = with pkgs; [
-        #   wget vim
+        #   vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
+        #   wget
         #   firefox
         # ];
 
diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix
index d81d6c6cb9b..c88cc693061 100644
--- a/nixos/modules/misc/documentation.nix
+++ b/nixos/modules/misc/documentation.nix
@@ -98,7 +98,7 @@ in
 
           See "Multiple-output packages" chapter in the nixpkgs manual for more info.
         '';
-        # which is at ../../../doc/multiple-output.xml
+        # which is at ../../../doc/multiple-output.chapter.md
       };
 
       man.enable = mkOption {
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index a0f5ce72f33..1fd56adfe10 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -71,7 +71,7 @@ in
       #utmp = 29; # unused
       # ddclient = 30; # converted to DynamicUser = true
       davfs2 = 31;
-      #disnix = 33; # module removed
+      disnix = 33;
       osgi = 34;
       tor = 35;
       cups = 36;
@@ -387,7 +387,7 @@ in
       utmp = 29;
       # ddclient = 30; # converted to DynamicUser = true
       davfs2 = 31;
-      #disnix = 33; # module removed
+      disnix = 33;
       osgi = 34;
       tor = 35;
       #cups = 36; # unused
diff --git a/nixos/modules/misc/meta.nix b/nixos/modules/misc/meta.nix
index be3f4cbbcfe..1410e33342a 100644
--- a/nixos/modules/misc/meta.nix
+++ b/nixos/modules/misc/meta.nix
@@ -47,9 +47,9 @@ in
       doc = mkOption {
         type = docFile;
         internal = true;
-        example = "./meta.xml";
+        example = "./meta.chapter.xml";
         description = ''
-          Documentation prologe for the set of options of each module.  This
+          Documentation prologue for the set of options of each module.  This
           option should be defined at most once per module.
         '';
       };
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index d40edb2408f..0c0935a7992 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -49,6 +49,7 @@
   ./hardware/i2c.nix
   ./hardware/sensor/hddtemp.nix
   ./hardware/sensor/iio.nix
+  ./hardware/keyboard/teck.nix
   ./hardware/keyboard/zsa.nix
   ./hardware/ksm.nix
   ./hardware/ledger.nix
@@ -67,12 +68,13 @@
   ./hardware/steam-hardware.nix
   ./hardware/system-76.nix
   ./hardware/tuxedo-keyboard.nix
+  ./hardware/ubertooth.nix
   ./hardware/usb-wwan.nix
   ./hardware/onlykey.nix
   ./hardware/opentabletdriver.nix
+  ./hardware/sata.nix
   ./hardware/wooting.nix
   ./hardware/uinput.nix
-  ./hardware/video/amdgpu.nix
   ./hardware/video/amdgpu-pro.nix
   ./hardware/video/ati.nix
   ./hardware/video/capture/mwprocapture.nix
@@ -91,6 +93,7 @@
   ./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
@@ -111,6 +114,9 @@
   ./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
@@ -124,11 +130,14 @@
   ./programs/dconf.nix
   ./programs/digitalbitbox/default.nix
   ./programs/dmrconfig.nix
+  ./programs/droidcam.nix
   ./programs/environment.nix
   ./programs/evince.nix
+  ./programs/feedbackd.nix
   ./programs/file-roller.nix
   ./programs/firejail.nix
   ./programs/fish.nix
+  ./programs/flexoptix-app.nix
   ./programs/freetds.nix
   ./programs/fuse.nix
   ./programs/geary.nix
@@ -155,8 +164,10 @@
   ./programs/nm-applet.nix
   ./programs/npm.nix
   ./programs/oblogout.nix
+  ./programs/partition-manager.nix
   ./programs/plotinus.nix
   ./programs/proxychains.nix
+  ./programs/phosh.nix
   ./programs/qt5ct.nix
   ./programs/screen.nix
   ./programs/sedutil.nix
@@ -177,6 +188,7 @@
   ./programs/tmux.nix
   ./programs/traceroute.nix
   ./programs/tsm-client.nix
+  ./programs/turbovnc.nix
   ./programs/udevil.nix
   ./programs/usbtop.nix
   ./programs/vim.nix
@@ -229,6 +241,7 @@
   ./services/audio/alsa.nix
   ./services/audio/jack.nix
   ./services/audio/icecast.nix
+  ./services/audio/jmusicbot.nix
   ./services/audio/liquidsoap.nix
   ./services/audio/mpd.nix
   ./services/audio/mpdscribble.nix
@@ -242,6 +255,7 @@
   ./services/backup/automysqlbackup.nix
   ./services/backup/bacula.nix
   ./services/backup/borgbackup.nix
+  ./services/backup/borgmatic.nix
   ./services/backup/duplicati.nix
   ./services/backup/duplicity.nix
   ./services/backup/mysql-backup.nix
@@ -282,6 +296,7 @@
   ./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
@@ -351,6 +366,7 @@
   ./services/development/jupyter/default.nix
   ./services/development/jupyterhub/default.nix
   ./services/development/lorri.nix
+  ./services/display-managers/greetd.nix
   ./services/editors/emacs.nix
   ./services/editors/infinoted.nix
   ./services/games/factorio.nix
@@ -358,6 +374,7 @@
   ./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
@@ -457,7 +474,10 @@
   ./services/misc/couchpotato.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
@@ -490,8 +510,11 @@
   ./services/misc/logkeys.nix
   ./services/misc/leaps.nix
   ./services/misc/lidarr.nix
+  ./services/misc/lifecycled.nix
   ./services/misc/mame.nix
   ./services/misc/matrix-appservice-discord.nix
+  ./services/misc/matrix-appservice-irc.nix
+  ./services/misc/matrix-dendrite.nix
   ./services/misc/matrix-synapse.nix
   ./services/misc/mautrix-telegram.nix
   ./services/misc/mbpfan.nix
@@ -507,11 +530,14 @@
   ./services/misc/nzbget.nix
   ./services/misc/nzbhydra2.nix
   ./services/misc/octoprint.nix
+  ./services/misc/ombi.nix
   ./services/misc/osrm.nix
   ./services/misc/packagekit.nix
   ./services/misc/paperless.nix
   ./services/misc/parsoid.nix
   ./services/misc/plex.nix
+  ./services/misc/plikd.nix
+  ./services/misc/podgrab.nix
   ./services/misc/tautulli.nix
   ./services/misc/pinnwand.nix
   ./services/misc/pykms.nix
@@ -612,6 +638,7 @@
   ./services/network-filesystems/xtreemfs.nix
   ./services/network-filesystems/ceph.nix
   ./services/networking/3proxy.nix
+  ./services/networking/adguardhome.nix
   ./services/networking/amuled.nix
   ./services/networking/aria2.nix
   ./services/networking/asterisk.nix
@@ -635,6 +662,7 @@
   ./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
@@ -644,6 +672,7 @@
   ./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
@@ -665,6 +694,7 @@
   ./services/networking/gnunet.nix
   ./services/networking/go-neb.nix
   ./services/networking/go-shadowsocks2.nix
+  ./services/networking/gobgpd.nix
   ./services/networking/gogoclient.nix
   ./services/networking/gvpe.nix
   ./services/networking/hans.nix
@@ -676,9 +706,13 @@
   ./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/jicofo.nix
   ./services/networking/jitsi-videobridge.nix
@@ -710,6 +744,7 @@
   ./services/networking/nar-serve.nix
   ./services/networking/nat.nix
   ./services/networking/ndppd.nix
+  ./services/networking/nebula.nix
   ./services/networking/networkmanager.nix
   ./services/networking/nextdns.nix
   ./services/networking/nftables.nix
@@ -745,7 +780,6 @@
   ./services/networking/prayer.nix
   ./services/networking/privoxy.nix
   ./services/networking/prosody.nix
-  ./services/networking/quagga.nix
   ./services/networking/quassel.nix
   ./services/networking/quorum.nix
   ./services/networking/quicktun.nix
@@ -848,6 +882,7 @@
   ./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
@@ -878,8 +913,11 @@
   ./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/convos.nix
   ./services/web-apps/cryptpad.nix
+  ./services/web-apps/discourse.nix
   ./services/web-apps/documize.nix
   ./services/web-apps/dokuwiki.nix
   ./services/web-apps/engelsystem.nix
@@ -916,6 +954,7 @@
   ./services/web-apps/selfoss.nix
   ./services/web-apps/shiori.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
@@ -937,10 +976,12 @@
   ./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/shellinabox.nix
   ./services/web-servers/tomcat.nix
   ./services/web-servers/traefik.nix
+  ./services/web-servers/trafficserver.nix
   ./services/web-servers/ttyd.nix
   ./services/web-servers/uwsgi.nix
   ./services/web-servers/varnish/default.nix
@@ -1051,6 +1092,7 @@
   ./testing/service-runner.nix
   ./virtualisation/anbox.nix
   ./virtualisation/container-config.nix
+  ./virtualisation/containerd.nix
   ./virtualisation/containers.nix
   ./virtualisation/nixos-containers.nix
   ./virtualisation/oci-containers.nix
diff --git a/nixos/modules/profiles/all-hardware.nix b/nixos/modules/profiles/all-hardware.nix
index d460c52dbef..c7a13974a51 100644
--- a/nixos/modules/profiles/all-hardware.nix
+++ b/nixos/modules/profiles/all-hardware.nix
@@ -46,11 +46,66 @@ in
       # VMware support.
       "mptspi" "vmxnet3" "vsock"
     ] ++ lib.optional platform.isx86 "vmw_balloon"
-    ++ lib.optionals (!platform.isAarch64) [ # not sure where else they're missing
+    ++ lib.optionals (!platform.isAarch64 && !platform.isAarch32) [ # not sure where else they're missing
       "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.
diff --git a/nixos/modules/programs/bash/bash-completion.nix b/nixos/modules/programs/bash/bash-completion.nix
new file mode 100644
index 00000000000..f07b1b636ef
--- /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
index 1b3254b54a5..908ab34b08d 100644
--- a/nixos/modules/programs/bash/bash.nix
+++ b/nixos/modules/programs/bash/bash.nix
@@ -11,31 +11,6 @@ let
 
   cfg = config.programs.bash;
 
-  bashCompletion = optionalString cfg.enableCompletion ''
-    # 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
-  '';
-
-  lsColors = optionalString cfg.enableLsColors ''
-    eval "$(${pkgs.coreutils}/bin/dircolors -b)"
-  '';
-
   bashAliases = concatStringsSep "\n" (
     mapAttrsFlatten (k: v: "alias ${k}=${escapeShellArg v}")
       (filterAttrs (k: v: v != null) cfg.shellAliases)
@@ -123,20 +98,13 @@ in
         type = types.lines;
       };
 
-      enableCompletion = mkOption {
-        default = true;
-        description = ''
-          Enable Bash completion for all interactive bash shells.
-        '';
-        type = types.bool;
-      };
-
-      enableLsColors = mkOption {
-        default = true;
+      promptPluginInit = mkOption {
+        default = "";
         description = ''
-          Enable extra colors in directory listings.
+          Shell script code used to initialise bash prompt plugins.
         '';
-        type = types.bool;
+        type = types.lines;
+        internal = true;
       };
 
     };
@@ -167,8 +135,7 @@ in
         set +h
 
         ${cfg.promptInit}
-        ${bashCompletion}
-        ${lsColors}
+        ${cfg.promptPluginInit}
         ${bashAliases}
 
         ${cfge.interactiveShellInit}
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..378144f598b
--- /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; [ metadark ];
+  };
+}
diff --git a/nixos/modules/programs/ccache.nix b/nixos/modules/programs/ccache.nix
index 3c9e64932f1..d672e1da017 100644
--- a/nixos/modules/programs/ccache.nix
+++ b/nixos/modules/programs/ccache.nix
@@ -17,7 +17,7 @@ in {
       type = types.listOf types.str;
       description = "Nix top-level packages to be compiled using CCache";
       default = [];
-      example = [ "wxGTK30" "qt48" "ffmpeg_3_3" "libav_all" ];
+      example = [ "wxGTK30" "ffmpeg" "libav_all" ];
     };
   };
 
diff --git a/nixos/modules/programs/command-not-found/command-not-found.nix b/nixos/modules/programs/command-not-found/command-not-found.nix
index d8394bf73a2..79786584c66 100644
--- a/nixos/modules/programs/command-not-found/command-not-found.nix
+++ b/nixos/modules/programs/command-not-found/command-not-found.nix
@@ -14,10 +14,8 @@ let
     dir = "bin";
     src = ./command-not-found.pl;
     isExecutable = true;
-    inherit (pkgs) perl;
     inherit (cfg) dbPath;
-    perlFlags = concatStrings (map (path: "-I ${path}/${pkgs.perl.libPrefix} ")
-      [ pkgs.perlPackages.DBI pkgs.perlPackages.DBDSQLite pkgs.perlPackages.StringShellQuote ]);
+    perl = pkgs.perl.withPackages (p: [ p.DBDSQLite p.StringShellQuote ]);
   };
 
 in
diff --git a/nixos/modules/programs/command-not-found/command-not-found.pl b/nixos/modules/programs/command-not-found/command-not-found.pl
index 7515dd975c3..6e275bcc8be 100644
--- a/nixos/modules/programs/command-not-found/command-not-found.pl
+++ b/nixos/modules/programs/command-not-found/command-not-found.pl
@@ -1,4 +1,4 @@
-#! @perl@/bin/perl -w @perlFlags@
+#! @perl@/bin/perl -w
 
 use strict;
 use DBI;
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/feedbackd.nix b/nixos/modules/programs/feedbackd.nix
new file mode 100644
index 00000000000..bb14489a6f4
--- /dev/null
+++ b/nixos/modules/programs/feedbackd.nix
@@ -0,0 +1,32 @@
+{ 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;
+      };
+    };
+  };
+  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/fish.nix b/nixos/modules/programs/fish.nix
index 392f06eb933..8dd7101947f 100644
--- a/nixos/modules/programs/fish.nix
+++ b/nixos/modules/programs/fish.nix
@@ -8,6 +8,11 @@ let
 
   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)
@@ -83,6 +88,18 @@ in
         '';
       };
 
+      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 = ''
@@ -205,6 +222,7 @@ in
         # 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"}
diff --git a/nixos/modules/programs/fish_completion-generator.patch b/nixos/modules/programs/fish_completion-generator.patch
index 997f38c5066..fa207e484c9 100644
--- a/nixos/modules/programs/fish_completion-generator.patch
+++ b/nixos/modules/programs/fish_completion-generator.patch
@@ -1,13 +1,14 @@
 --- a/create_manpage_completions.py
 +++ b/create_manpage_completions.py
-@@ -844,10 +844,6 @@ def parse_manpage_at_path(manpage_path, output_directory):
+@@ -879,10 +879,6 @@ def parse_manpage_at_path(manpage_path, output_directory):
+                 )
+                 return False
  
-             built_command_output.insert(0, "# " + CMDNAME)
+-        # 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
  
--            # Output the magic word Autogenerated so we can tell if we can overwrite this
--            built_command_output.insert(
--                1, "# Autogenerated from man page " + manpage_path
--            )
-             # built_command_output.insert(2, "# using " + parser.__class__.__name__) # XXX MISATTRIBUTES THE CULPABILE PARSER! Was really using Type2 but reporting TypeDeroffManParser
- 
-             for line in built_command_output:
+         for line in built_command_output:
+
diff --git a/nixos/modules/programs/flexoptix-app.nix b/nixos/modules/programs/flexoptix-app.nix
new file mode 100644
index 00000000000..93dcdfeb514
--- /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 = "\${pkgs.flexoptix-app}";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    services.udev.packages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/programs/less.nix b/nixos/modules/programs/less.nix
index 75b3e707d57..09cb6030e66 100644
--- a/nixos/modules/programs/less.nix
+++ b/nixos/modules/programs/less.nix
@@ -40,7 +40,7 @@ in
       configFile = mkOption {
         type = types.nullOr types.path;
         default = null;
-        example = literalExample "$${pkgs.my-configs}/lesskey";
+        example = literalExample "\${pkgs.my-configs}/lesskey";
         description = ''
           Path to lesskey configuration file.
 
diff --git a/nixos/modules/programs/mininet.nix b/nixos/modules/programs/mininet.nix
index ecc924325e6..6e90e7669ac 100644
--- a/nixos/modules/programs/mininet.nix
+++ b/nixos/modules/programs/mininet.nix
@@ -8,7 +8,7 @@ let
   cfg  = config.programs.mininet;
 
   generatedPath = with pkgs; makeSearchPath "bin"  [
-    iperf ethtool iproute socat
+    iperf ethtool iproute2 socat
   ];
 
   pyEnv = pkgs.python.withPackages(ps: [ ps.mininet-python ]);
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..f6faf7990dd
--- /dev/null
+++ b/nixos/modules/programs/phosh.nix
@@ -0,0 +1,167 @@
+{ 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";
+    type = "Application";
+    desktopName = "On-screen keyboard";
+    exec = "${pkgs.squeekboard}/bin/squeekboard";
+    categories = "GNOME;Core;";
+    extraEntries = ''
+      OnlyShowIn=GNOME;
+      NoDisplay=true
+      X-GNOME-Autostart-Phase=Panel
+      X-GNOME-Provides=inputmethod
+      X-GNOME-Autostart-Notify=true
+      X-GNOME-AutoRestart=true
+    '';
+  };
+
+  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
+    ];
+
+    programs.feedbackd.enable = true;
+
+    # https://source.puri.sm/Librem5/phosh/-/issues/303
+    security.pam.services.phosh = {
+      text = ''
+        auth    requisite       pam_nologin.so
+        auth    required        pam_succeed_if.so user != root quiet_success
+        auth    required        pam_securetty.so
+        auth    requisite       pam_nologin.so
+      '';
+    };
+
+    services.gnome3.core-shell.enable = true;
+    services.gnome3.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/steam.nix b/nixos/modules/programs/steam.nix
index 6e9b7729ad6..ff4deba2bf0 100644
--- a/nixos/modules/programs/steam.nix
+++ b/nixos/modules/programs/steam.nix
@@ -12,11 +12,30 @@ let
       else [ package32 ] ++ extraPackages32;
   };
 in {
-  options.programs.steam.enable = mkEnableOption "steam";
+  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;
     };
 
@@ -26,6 +45,18 @@ in {
     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
index 038d76c6c92..107e783c0c2 100644
--- a/nixos/modules/programs/sway.nix
+++ b/nixos/modules/programs/sway.nix
@@ -90,7 +90,7 @@ in {
         rxvt-unicode # For backward compatibility (old default terminal)
       ];
       defaultText = literalExample ''
-        with pkgs; [ swaylock swayidle xwayland rxvt-unicode dmenu ];
+        with pkgs; [ swaylock swayidle rxvt-unicode alacritty dmenu ];
       '';
       example = literalExample ''
         with pkgs; [
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/rename.nix b/nixos/modules/rename.nix
index 9f1efc46279..233e3ee848b 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -18,6 +18,7 @@ with lib;
 
     # Completely removed modules
     (mkRemovedOptionModule [ "fonts" "fontconfig" "penultimate" ] "The corresponding package has removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "quagga" ] "the corresponding package has been removed from nixpkgs")
     (mkRemovedOptionModule [ "services" "chronos" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "deepin" ] "The corresponding packages were removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "firefox" "syncserver" "user" ] "")
diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix
index c33a92580d4..eb3599b924d 100644
--- a/nixos/modules/security/acme.nix
+++ b/nixos/modules/security/acme.nix
@@ -24,7 +24,7 @@ let
       Type = "oneshot";
       User = "acme";
       Group = mkDefault "acme";
-      UMask = 0023;
+      UMask = 0022;
       StateDirectoryMode = 750;
       ProtectSystem = "full";
       PrivateTmp = true;
@@ -235,7 +235,7 @@ let
       # https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099
       wantedBy = optionals (!config.boot.isContainer) [ "multi-user.target" ];
 
-      path = with pkgs; [ lego coreutils diffutils ];
+      path = with pkgs; [ lego coreutils diffutils openssl ];
 
       serviceConfig = commonServiceConfig // {
         Group = data.group;
@@ -274,10 +274,44 @@ let
       script = ''
         set -euxo 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 cfg.validMinDays} ]]
+        }
+
         ${optionalString (data.webroot != null) ''
-          # Ensure the webroot exists
-          mkdir -p '${data.webroot}/.well-known/acme-challenge'
-          chown 'acme:${data.group}' ${data.webroot}/{.well-known,.well-known/acme-challenge}
+          # 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
@@ -288,8 +322,14 @@ let
           # When domains are updated, there's no need to do a full
           # Lego run, but it's likely renew won't work if days is too low.
           if [ -e certificates/domainhash.txt ] && cmp -s domainhash.txt certificates/domainhash.txt; then
-            lego ${renewOpts} --days ${toString cfg.validMinDays}
+            if is_expiration_skippable out/full.pem; then
+              echo 1>&2 "nixos-acme: skipping renewal because expiration isn't within the coming ${toString cfg.validMinDays} days"
+            else
+              echo 1>&2 "nixos-acme: renewing now, because certificate expires within the configured ${toString cfg.validMinDays} days"
+              lego ${renewOpts} --days ${toString cfg.validMinDays}
+            fi
           else
+            echo 1>&2 "certificate domain(s) have changed; will renew now"
             # Any number > 90 works, but this one is over 9000 ;-)
             lego ${renewOpts} --days 9001
           fi
diff --git a/nixos/modules/services/audio/jmusicbot.nix b/nixos/modules/services/audio/jmusicbot.nix
new file mode 100644
index 00000000000..f573bd2ab8d
--- /dev/null
+++ b/nixos/modules/services/audio/jmusicbot.nix
@@ -0,0 +1,41 @@
+{ 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";
+
+      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 = "${pkgs.jmusicbot}/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/mpd.nix b/nixos/modules/services/audio/mpd.nix
index 9f01e29dd0e..eee6c5f423d 100644
--- a/nixos/modules/services/audio/mpd.nix
+++ b/nixos/modules/services/audio/mpd.nix
@@ -213,7 +213,9 @@ in {
       description = "Music Player Daemon Socket";
       wantedBy = [ "sockets.target" ];
       listenStreams = [
-        "${optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:"}${toString cfg.network.port}"
+        (if pkgs.lib.hasPrefix "/" cfg.network.listenAddress
+          then cfg.network.listenAddress
+          else "${optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:"}${toString cfg.network.port}")
       ];
       socketConfig = {
         Backlog = 5;
diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix
index a261b876078..f96b5f3e194 100644
--- a/nixos/modules/services/audio/snapserver.nix
+++ b/nixos/modules/services/audio/snapserver.nix
@@ -65,7 +65,7 @@ let
 
 in {
   imports = [
-    (mkRenamedOptionModule [ "services" "snapserver" "controlPort"] [ "services" "snapserver" "tcp" "port" ])
+    (mkRenamedOptionModule [ "services" "snapserver" "controlPort" ] [ "services" "snapserver" "tcp" "port" ])
   ];
 
   ###### interface
@@ -200,12 +200,21 @@ in {
             location = mkOption {
               type = types.oneOf [ types.path types.str ];
               description = ''
-                The location of the pipe, file, Librespot/Airplay/process binary, or a TCP address.
-                Use an empty string for alsa.
+                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 = literalExample ''
+                "/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" ];
+              type = types.enum [ "pipe" "librespot" "airplay" "file" "process" "tcp" "alsa" "spotify" "meta" ];
               default = "pipe";
               description = ''
                 The type of input stream.
diff --git a/nixos/modules/services/audio/spotifyd.nix b/nixos/modules/services/audio/spotifyd.nix
index a589153248f..9279a03aed4 100644
--- a/nixos/modules/services/audio/spotifyd.nix
+++ b/nixos/modules/services/audio/spotifyd.nix
@@ -27,6 +27,7 @@ in
       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";
diff --git a/nixos/modules/services/backup/borgbackup.nix b/nixos/modules/services/backup/borgbackup.nix
index be661b201f0..18fb29fd72a 100644
--- a/nixos/modules/services/backup/borgbackup.nix
+++ b/nixos/modules/services/backup/borgbackup.nix
@@ -169,6 +169,7 @@ let
         (map (mkAuthorizedKey cfg false) cfg.authorizedKeys
         ++ map (mkAuthorizedKey cfg true) cfg.authorizedKeysAppendOnly);
       useDefaultShell = true;
+      isSystemUser = true;
     };
     groups.${cfg.group} = { };
   };
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/restic.nix b/nixos/modules/services/backup/restic.nix
index 573f0efa9da..ac57f271526 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -93,10 +93,12 @@ in
         };
 
         paths = mkOption {
-          type = types.listOf types.str;
-          default = [];
+          type = types.nullOr (types.listOf types.str);
+          default = null;
           description = ''
-            Which paths to backup.
+            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"
@@ -217,7 +219,7 @@ in
           resticCmd = "${pkgs.restic}/bin/restic${extraOptions}";
           filesFromTmpFile = "/run/restic-backups-${name}/includes";
           backupPaths = if (backup.dynamicFilesFrom == null)
-                        then concatStringsSep " " backup.paths
+                        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) )
@@ -243,7 +245,8 @@ in
           restartIfChanged = false;
           serviceConfig = {
             Type = "oneshot";
-            ExecStart = [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ] ++ pruneCmd;
+            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}";
diff --git a/nixos/modules/services/cluster/k3s/default.nix b/nixos/modules/services/cluster/k3s/default.nix
index e62fbc94415..5ab0286a38a 100644
--- a/nixos/modules/services/cluster/k3s/default.nix
+++ b/nixos/modules/services/cluster/k3s/default.nix
@@ -83,7 +83,8 @@ in
 
     systemd.services.k3s = {
       description = "k3s service";
-      after = mkIf cfg.docker [ "docker.service" ];
+      after = [ "network.service" "firewall.service" ] ++ (optional cfg.docker "docker.service");
+      wants = [ "network.service" "firewall.service" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
         # See: https://github.com/rancher/k3s/blob/dddbd16305284ae4bd14c0aade892412310d7edc/install.sh#L197
@@ -92,6 +93,10 @@ in
         Delegate = "yes";
         Restart = "always";
         RestartSec = "5s";
+        LimitNOFILE = 1048576;
+        LimitNPROC = "infinity";
+        LimitCORE = "infinity";
+        TasksMax = "infinity";
         ExecStart = concatStringsSep " \\\n " (
           [
             "${cfg.package}/bin/k3s ${cfg.role}"
diff --git a/nixos/modules/services/cluster/kubernetes/addons/dns.nix b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
index f12e866930d..24d86628b21 100644
--- a/nixos/modules/services/cluster/kubernetes/addons/dns.nix
+++ b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
@@ -3,7 +3,7 @@
 with lib;
 
 let
-  version = "1.6.4";
+  version = "1.7.1";
   cfg = config.services.kubernetes.addons.dns;
   ports = {
     dns = 10053;
@@ -55,9 +55,9 @@ in {
       type = types.attrs;
       default = {
         imageName = "coredns/coredns";
-        imageDigest = "sha256:493ee88e1a92abebac67cbd4b5658b4730e0f33512461442d8d9214ea6734a9b";
+        imageDigest = "sha256:4a6e0769130686518325b21b0c1d0688b54e7c79244d48e1b15634e98e40c6ef";
         finalImageTag = version;
-        sha256 = "0fm9zdjavpf5hni8g7fkdd3csjbhd7n7py7llxjc66sbii087028";
+        sha256 = "02r440xcdsgi137k5lmmvp0z5w5fmk8g9mysq5pnysq1wl8sj6mw";
       };
     };
   };
@@ -156,7 +156,6 @@ in {
             health :${toString ports.health}
             kubernetes ${cfg.clusterDomain} in-addr.arpa ip6.arpa {
               pods insecure
-              upstream
               fallthrough in-addr.arpa ip6.arpa
             }
             prometheus :${toString ports.metrics}
diff --git a/nixos/modules/services/cluster/kubernetes/apiserver.nix b/nixos/modules/services/cluster/kubernetes/apiserver.nix
index 95bdb4c0d14..f1531caa754 100644
--- a/nixos/modules/services/cluster/kubernetes/apiserver.nix
+++ b/nixos/modules/services/cluster/kubernetes/apiserver.nix
@@ -145,7 +145,7 @@ in
     extraOpts = mkOption {
       description = "Kubernetes apiserver extra command line options.";
       default = "";
-      type = str;
+      type = separatedString " ";
     };
 
     extraSANs = mkOption {
@@ -238,14 +238,40 @@ in
       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 = ''
-        Kubernetes apiserver PEM-encoded x509 RSA private or public key file,
-        used to verify ServiceAccount tokens. By default tls private key file
-        is used.
+        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
       '';
-      default = null;
-      type = nullOr path;
+      type = path;
     };
 
     serviceClusterIpRange = mkOption {
@@ -357,8 +383,10 @@ in
               ${optionalString (cfg.runtimeConfig != "")
                 "--runtime-config=${cfg.runtimeConfig}"} \
               --secure-port=${toString cfg.securePort} \
-              ${optionalString (cfg.serviceAccountKeyFile!=null)
-                "--service-account-key-file=${cfg.serviceAccountKeyFile}"} \
+              --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)
diff --git a/nixos/modules/services/cluster/kubernetes/controller-manager.nix b/nixos/modules/services/cluster/kubernetes/controller-manager.nix
index a99ef6640e9..0c81fa9ae49 100644
--- a/nixos/modules/services/cluster/kubernetes/controller-manager.nix
+++ b/nixos/modules/services/cluster/kubernetes/controller-manager.nix
@@ -38,7 +38,7 @@ in
     extraOpts = mkOption {
       description = "Kubernetes controller manager extra command line options.";
       default = "";
-      type = str;
+      type = separatedString " ";
     };
 
     featureGates = mkOption {
diff --git a/nixos/modules/services/cluster/kubernetes/default.nix b/nixos/modules/services/cluster/kubernetes/default.nix
index 3a11a6513a4..19edc338bba 100644
--- a/nixos/modules/services/cluster/kubernetes/default.nix
+++ b/nixos/modules/services/cluster/kubernetes/default.nix
@@ -5,6 +5,29 @@ with lib;
 let
   cfg = config.services.kubernetes;
 
+  defaultContainerdConfigFile = pkgs.writeText "containerd.toml" ''
+    version = 2
+    root = "/var/lib/containerd/daemon"
+    state = "/var/run/containerd/daemon"
+    oom_score = 0
+
+    [grpc]
+      address = "/var/run/containerd/containerd.sock"
+
+    [plugins."io.containerd.grpc.v1.cri"]
+      sandbox_image = "pause:latest"
+
+    [plugins."io.containerd.grpc.v1.cri".cni]
+      bin_dir = "/opt/cni/bin"
+      max_conf_num = 0
+
+    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
+      runtime_type = "io.containerd.runc.v2"
+
+    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes."io.containerd.runc.v2".options]
+      SystemdCgroup = true
+  '';
+
   mkKubeConfig = name: conf: pkgs.writeText "${name}-kubeconfig" (builtins.toJSON {
     apiVersion = "v1";
     kind = "Config";
@@ -222,14 +245,9 @@ in {
     })
 
     (mkIf cfg.kubelet.enable {
-      virtualisation.docker = {
+      virtualisation.containerd = {
         enable = mkDefault true;
-
-        # kubernetes needs access to logs
-        logDriver = mkDefault "json-file";
-
-        # iptables must be disabled for kubernetes
-        extraOptions = "--iptables=false --ip-masq=false";
+        configFile = mkDefault defaultContainerdConfigFile;
       };
     })
 
@@ -269,7 +287,6 @@ in {
       users.users.kubernetes = {
         uid = config.ids.uids.kubernetes;
         description = "Kubernetes user";
-        extraGroups = [ "docker" ];
         group = "kubernetes";
         home = cfg.dataDir;
         createHome = true;
diff --git a/nixos/modules/services/cluster/kubernetes/flannel.nix b/nixos/modules/services/cluster/kubernetes/flannel.nix
index 548ffed1ddb..3f55719027f 100644
--- a/nixos/modules/services/cluster/kubernetes/flannel.nix
+++ b/nixos/modules/services/cluster/kubernetes/flannel.nix
@@ -8,16 +8,6 @@ let
 
   # we want flannel to use kubernetes itself as configuration backend, not direct etcd
   storageBackend = "kubernetes";
-
-  # needed for flannel to pass options to docker
-  mkDockerOpts = pkgs.runCommand "mk-docker-opts" {
-    buildInputs = [ pkgs.makeWrapper ];
-  } ''
-    mkdir -p $out
-
-    # bashInteractive needed for `compgen`
-    makeWrapper ${pkgs.bashInteractive}/bin/bash $out/mk-docker-opts --add-flags "${pkgs.kubernetes}/bin/mk-docker-opts.sh"
-  '';
 in
 {
   ###### interface
@@ -43,43 +33,17 @@ in
         cniVersion = "0.3.1";
         delegate = {
           isDefaultGateway = true;
-          bridge = "docker0";
+          bridge = "mynet";
         };
       }];
     };
 
-    systemd.services.mk-docker-opts = {
-      description = "Pre-Docker Actions";
-      path = with pkgs; [ gawk gnugrep ];
-      script = ''
-        ${mkDockerOpts}/mk-docker-opts -d /run/flannel/docker
-        systemctl restart docker
-      '';
-      serviceConfig.Type = "oneshot";
-    };
-
-    systemd.paths.flannel-subnet-env = {
-      wantedBy = [ "flannel.service" ];
-      pathConfig = {
-        PathModified = "/run/flannel/subnet.env";
-        Unit = "mk-docker-opts.service";
-      };
-    };
-
-    systemd.services.docker = {
-      environment.DOCKER_OPTS = "-b none";
-      serviceConfig.EnvironmentFile = "-/run/flannel/docker";
-    };
-
-    # read environment variables generated by mk-docker-opts
-    virtualisation.docker.extraOptions = "$DOCKER_OPTS";
-
     networking = {
       firewall.allowedUDPPorts = [
         8285  # flannel udp
         8472  # flannel vxlan
       ];
-      dhcpcd.denyInterfaces = [ "docker*" "flannel*" ];
+      dhcpcd.denyInterfaces = [ "mynet*" "flannel*" ];
     };
 
     services.kubernetes.pki.certs = {
diff --git a/nixos/modules/services/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix
index 479027f1b27..a428a60800c 100644
--- a/nixos/modules/services/cluster/kubernetes/kubelet.nix
+++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix
@@ -23,7 +23,7 @@ let
     name = "pause";
     tag = "latest";
     contents = top.package.pause;
-    config.Cmd = "/bin/pause";
+    config.Cmd = ["/bin/pause"];
   };
 
   kubeconfig = top.lib.mkKubeConfig "kubelet" cfg.kubeconfig;
@@ -125,12 +125,24 @@ in
       };
     };
 
+    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:///var/run/containerd/containerd.sock";
+    };
+
     enable = mkEnableOption "Kubernetes kubelet.";
 
     extraOpts = mkOption {
       description = "Kubernetes kubelet extra command line options.";
       default = "";
-      type = str;
+      type = separatedString " ";
     };
 
     featureGates = mkOption {
@@ -235,18 +247,26 @@ in
   ###### 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 = [ "network.target" "docker.service" "kube-apiserver.service" ];
+        after = [ "containerd.service" "network.target" "kube-apiserver.service" ];
         path = with pkgs; [
           gitMinimal
           openssh
-          docker
           util-linux
-          iproute
+          iproute2
           ethtool
           thin-provisioning-tools
           iptables
@@ -254,8 +274,12 @@ in
         ] ++ lib.optional config.boot.zfs.enabled config.boot.zfs.package ++ top.path;
         preStart = ''
           ${concatMapStrings (img: ''
-            echo "Seeding docker image: ${img}"
-            docker load <${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
@@ -306,6 +330,9 @@ in
             ${optionalString (cfg.tlsKeyFile != null)
               "--tls-private-key-file=${cfg.tlsKeyFile}"} \
             ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
+            --container-runtime=${cfg.containerRuntime} \
+            --container-runtime-endpoint=${cfg.containerRuntimeEndpoint} \
+            --cgroup-driver=systemd \
             ${cfg.extraOpts}
           '';
           WorkingDirectory = top.dataDir;
@@ -315,7 +342,7 @@ in
       # Allways include cni plugins
       services.kubernetes.kubelet.cni.packages = [pkgs.cni-plugins];
 
-      boot.kernelModules = ["br_netfilter"];
+      boot.kernelModules = ["br_netfilter" "overlay"];
 
       services.kubernetes.kubelet.hostname = with config.networking;
         mkDefault (hostName + optionalString (domain != null) ".${domain}");
diff --git a/nixos/modules/services/cluster/kubernetes/pki.nix b/nixos/modules/services/cluster/kubernetes/pki.nix
index 933ae481e96..8de6a3ba0d8 100644
--- a/nixos/modules/services/cluster/kubernetes/pki.nix
+++ b/nixos/modules/services/cluster/kubernetes/pki.nix
@@ -361,6 +361,7 @@ in
           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;
diff --git a/nixos/modules/services/cluster/kubernetes/proxy.nix b/nixos/modules/services/cluster/kubernetes/proxy.nix
index 86d1dc2439b..7aa449f9aa2 100644
--- a/nixos/modules/services/cluster/kubernetes/proxy.nix
+++ b/nixos/modules/services/cluster/kubernetes/proxy.nix
@@ -25,7 +25,7 @@ in
     extraOpts = mkOption {
       description = "Kubernetes proxy extra command line options.";
       default = "";
-      type = str;
+      type = separatedString " ";
     };
 
     featureGates = mkOption {
diff --git a/nixos/modules/services/cluster/kubernetes/scheduler.nix b/nixos/modules/services/cluster/kubernetes/scheduler.nix
index 5f6113227d9..454c689759d 100644
--- a/nixos/modules/services/cluster/kubernetes/scheduler.nix
+++ b/nixos/modules/services/cluster/kubernetes/scheduler.nix
@@ -21,7 +21,7 @@ in
     extraOpts = mkOption {
       description = "Kubernetes scheduler extra command line options.";
       default = "";
-      type = str;
+      type = separatedString " ";
     };
 
     featureGates = mkOption {
diff --git a/nixos/modules/services/computing/slurm/slurm.nix b/nixos/modules/services/computing/slurm/slurm.nix
index 7363441e538..a3dee94e2dc 100644
--- a/nixos/modules/services/computing/slurm/slurm.nix
+++ b/nixos/modules/services/computing/slurm/slurm.nix
@@ -274,6 +274,15 @@ in
         '';
       };
 
+      etcSlurm = mkOption {
+        type = types.path;
+        internal = true;
+        default = etcSlurm;
+        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.
+        '';
+      };
 
     };
 
@@ -308,7 +317,7 @@ in
           #!/bin/sh
           if [ -z "$SLURM_CONF" ]
           then
-            SLURM_CONF="${etcSlurm}/slurm.conf" "$EXE" "\$@"
+            SLURM_CONF="${cfg.etcSlurm}/slurm.conf" "$EXE" "\$@"
           else
             "$EXE" "\$0"
           fi
@@ -394,9 +403,7 @@ in
       requires = [ "munged.service" "mysql.service" ];
 
       preStart = ''
-        cp ${slurmdbdConf} ${configPath}
-        chmod 600 ${configPath}
-        chown ${cfg.user} ${configPath}
+        install -m 600 -o ${cfg.user} -T ${slurmdbdConf} ${configPath}
         ${optionalString (cfg.dbdserver.storagePassFile != null) ''
           echo "StoragePass=$(cat ${cfg.dbdserver.storagePassFile})" \
             >> ${configPath}
diff --git a/nixos/modules/services/continuous-integration/buildkite-agents.nix b/nixos/modules/services/continuous-integration/buildkite-agents.nix
index b0045409ae6..3dd1c40aaa4 100644
--- a/nixos/modules/services/continuous-integration/buildkite-agents.nix
+++ b/nixos/modules/services/continuous-integration/buildkite-agents.nix
@@ -76,7 +76,7 @@ let
       };
 
       tags = mkOption {
-        type = types.attrsOf types.str;
+        type = types.attrsOf (types.either types.str (types.listOf types.str));
         default = {};
         example = { queue = "default"; docker = "true"; ruby2 ="true"; };
         description = ''
@@ -230,7 +230,11 @@ in
         ##     don't end up in the Nix store.
         preStart = let
           sshDir = "${cfg.dataDir}/.ssh";
-          tagStr = lib.concatStringsSep "," (lib.mapAttrsToList (name: value: "${name}=${value}") cfg.tags);
+          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}"
@@ -241,7 +245,7 @@ in
             token="$(cat ${toString cfg.tokenPath})"
             name="${cfg.name}"
             shell="${cfg.shell}"
-            tags="${tagStr}"
+            tags="${tagsStr}"
             build-path="${cfg.dataDir}/builds"
             hooks-path="${cfg.hooksPath}"
             ${cfg.extraConfig}
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..9627b723f8f
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/github-runner.nix
@@ -0,0 +1,299 @@
+{ 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}";
+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.
+      '';
+      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;
+    };
+
+    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 = literalExample ''[ "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 = [ ];
+    };
+  };
+
+  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 = "${pkgs.github-runner}/bin/runsvc.sh";
+
+        # Does the following, sequentially:
+        # - Copy the current and the previous `tokenFile` to the $RUNTIME_DIRECTORY
+        #   and make it accessible to the service user to allow for a content
+        #   comparison.
+        # - If the module configuration or the token has changed, clear the state directory.
+        # - Configure the runner.
+        # - Copy the configured `tokenFile` to the $STATE_DIRECTORY and make it
+        #   inaccessible to the service user.
+        # - 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);
+            currentConfigTokenFilename = ".current-token";
+            newConfigTokenFilename = ".new-token";
+            runnerCredFiles = [
+              ".credentials"
+              ".credentials_rsaparams"
+              ".runner"
+            ];
+            ownConfigTokens = writeScript "own-config-tokens" ''
+              # Copy current and new token file to runtime dir and make it accessible to the service user
+              cp ${escapeShellArg cfg.tokenFile} "$RUNTIME_DIRECTORY/${newConfigTokenFilename}"
+              chmod 600 "$RUNTIME_DIRECTORY/${newConfigTokenFilename}"
+              chown "$USER" "$RUNTIME_DIRECTORY/${newConfigTokenFilename}"
+
+              if [[ -e "$STATE_DIRECTORY/${currentConfigTokenFilename}" ]]; then
+                cp "$STATE_DIRECTORY/${currentConfigTokenFilename}" "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}"
+                chmod 600 "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}"
+                chown "$USER" "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}"
+              fi
+            '';
+            disownConfigTokens = writeScript "disown-config-tokens" ''
+              # Make the token inaccessible to the runner service user
+              chmod 600 "$STATE_DIRECTORY/${currentConfigTokenFilename}"
+              chown root:root "$STATE_DIRECTORY/${currentConfigTokenFilename}"
+            '';
+            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 \
+                "$RUNTIME_DIRECTORY"/{${currentConfigTokenFilename},${newConfigTokenFilename}} \
+                >/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
+              fi
+            '';
+            configureRunner = writeScript "configure" ''
+              empty=$(ls -A "$STATE_DIRECTORY")
+              if [[ -z "$empty" ]]; then
+                echo "Configuring GitHub Actions Runner"
+                token=$(< "$RUNTIME_DIRECTORY"/${newConfigTokenFilename})
+                RUNNER_ROOT="$STATE_DIRECTORY" ${pkgs.github-runner}/bin/config.sh \
+                  --unattended \
+                  --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 -f "$RUNTIME_DIRECTORY"/${currentConfigTokenFilename}
+                mv    "$RUNTIME_DIRECTORY"/${newConfigTokenFilename} "$STATE_DIRECTORY/${currentConfigTokenFilename}"
+
+                # 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 ]}") [
+            "+${ownConfigTokens}" # runs as root
+            unconfigureRunner
+            configureRunner
+            "+${disownConfigTokens}" # runs as root
+            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;
+
+        # 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/hercules-ci-agent/common.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
index 9f9b86ee61c..2f0b573e872 100644
--- a/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
@@ -37,15 +37,22 @@ let
         description = ''
           Number of tasks to perform simultaneously.
 
-          A task is a single derivation build or an evaluation.
+          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.int;
-        default = 4;
+        type = types.either types.ints.positive (types.enum [ "auto" ]);
+        default = "auto";
       };
       workDirectory = mkOption {
         description = ''
@@ -186,7 +193,18 @@ in
       # 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;
+    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
index e8a42e59de0..06c174e7d37 100644
--- a/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
@@ -68,7 +68,23 @@ in
     # Trusted user allows simplified configuration and better performance
     # when operating in a cluster.
     nix.trustedUsers = [ config.systemd.services.hercules-ci-agent.serviceConfig.User ];
-    services.hercules-ci-agent.settings.nixUserIsTrusted = true;
+    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;
diff --git a/nixos/modules/services/continuous-integration/hydra/default.nix b/nixos/modules/services/continuous-integration/hydra/default.nix
index 887a0cbf9a7..0103cd723d2 100644
--- a/nixos/modules/services/continuous-integration/hydra/default.nix
+++ b/nixos/modules/services/continuous-integration/hydra/default.nix
@@ -89,6 +89,11 @@ in
         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.
         '';
       };
 
@@ -275,6 +280,8 @@ in
       keep-outputs = true
       keep-derivations = true
 
+
+    '' + optionalString (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
@@ -284,7 +291,9 @@ in
       { wantedBy = [ "multi-user.target" ];
         requires = optional haveLocalDB "postgresql.service";
         after = optional haveLocalDB "postgresql.service";
-        environment = env;
+        environment = env // {
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-init";
+        };
         preStart = ''
           mkdir -p ${baseDir}
           chown hydra.hydra ${baseDir}
@@ -339,7 +348,9 @@ in
       { wantedBy = [ "multi-user.target" ];
         requires = [ "hydra-init.service" ];
         after = [ "hydra-init.service" ];
-        environment = serverEnv;
+        environment = serverEnv // {
+          HYDRA_DBI = "${serverEnv.HYDRA_DBI};application_name=hydra-server";
+        };
         restartTriggers = [ hydraConf ];
         serviceConfig =
           { ExecStart =
@@ -361,6 +372,7 @@ in
         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";
@@ -380,7 +392,9 @@ in
         after = [ "hydra-init.service" "network.target" ];
         path = with pkgs; [ hydra-package nettools jq ];
         restartTriggers = [ hydraConf ];
-        environment = env;
+        environment = env // {
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-evaluator";
+        };
         serviceConfig =
           { ExecStart = "@${hydra-package}/bin/hydra-evaluator hydra-evaluator";
             User = "hydra";
@@ -392,7 +406,9 @@ in
     systemd.services.hydra-update-gc-roots =
       { requires = [ "hydra-init.service" ];
         after = [ "hydra-init.service" ];
-        environment = env;
+        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";
@@ -403,7 +419,9 @@ in
     systemd.services.hydra-send-stats =
       { wantedBy = [ "multi-user.target" ];
         after = [ "hydra-init.service" ];
-        environment = env;
+        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";
@@ -417,6 +435,7 @@ in
         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";
diff --git a/nixos/modules/services/databases/cassandra.nix b/nixos/modules/services/databases/cassandra.nix
index d55a7db3915..820be5085de 100644
--- a/nixos/modules/services/databases/cassandra.nix
+++ b/nixos/modules/services/databases/cassandra.nix
@@ -1,79 +1,108 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
+  inherit (lib)
+    concatStringsSep
+    flip
+    literalExample
+    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";
-     } // (lib.optionalAttrs (cfg.seedAddresses != []) {
-       seed_provider = [{
-         class_name = "org.apache.cassandra.locator.SimpleSeedProvider";
-         parameters = [ { seeds = concatStringsSep "," cfg.seedAddresses; } ];
-       }];
-     }) // (lib.optionalAttrs (lib.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
-    ++ lib.optionals (cfg.jmxRoles != []) [
+
+  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}"
-    ]
-    ++ lib.optionals cfg.remoteJmx [
+    ] ++ optionals cfg.remoteJmx [
       "-Djava.rmi.server.hostname=${cfg.rpcAddress}"
     ];
-in {
+
+in
+{
   options.services.cassandra = {
+
     enable = mkEnableOption ''
       Apache Cassandra – Scalable and highly available database.
     '';
+
     clusterName = mkOption {
       type = types.str;
       default = "Test Cluster";
@@ -83,16 +112,19 @@ in {
         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";
@@ -100,6 +132,7 @@ in {
         Home directory for Apache Cassandra.
       '';
     };
+
     package = mkOption {
       type = types.package;
       default = pkgs.cassandra;
@@ -109,17 +142,19 @@ in {
         The Apache Cassandra package to use.
       '';
     };
+
     jvmOpts = mkOption {
       type = types.listOf types.str;
-      default = [];
+      default = [ ];
       description = ''
         Populate the JVM_OPT environment variable.
       '';
     };
+
     listenAddress = mkOption {
       type = types.nullOr types.str;
       default = "127.0.0.1";
-      example = literalExample "null";
+      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
@@ -136,6 +171,7 @@ in {
         Setting listen_address to 0.0.0.0 is always wrong.
       '';
     };
+
     listenInterface = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -146,10 +182,11 @@ in {
         supported.
       '';
     };
+
     rpcAddress = mkOption {
       type = types.nullOr types.str;
       default = "127.0.0.1";
-      example = literalExample "null";
+      example = null;
       description = ''
         The address or interface to bind the native transport server to.
 
@@ -167,6 +204,7 @@ in {
         internet. Firewall it if needed.
       '';
     };
+
     rpcInterface = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -176,6 +214,7 @@ in {
         correspond to a single address, IP aliasing is not supported.
       '';
     };
+
     logbackConfig = mkOption {
       type = types.lines;
       default = ''
@@ -197,6 +236,7 @@ in {
         XML logback configuration for cassandra
       '';
     };
+
     seedAddresses = mkOption {
       type = types.listOf types.str;
       default = [ "127.0.0.1" ];
@@ -207,6 +247,7 @@ in {
         Set to 127.0.0.1 for a single node cluster.
       '';
     };
+
     allowClients = mkOption {
       type = types.bool;
       default = true;
@@ -219,16 +260,19 @@ in {
         <literal>extraConfig</literal>.
       '';
     };
+
     extraConfig = mkOption {
       type = types.attrs;
-      default = {};
+      default = { };
       example =
-        { commitlog_sync_batch_window_in_ms = 3;
+        {
+          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 = "";
@@ -237,48 +281,53 @@ in {
         Extra shell lines to be appended onto cassandra-env.sh.
       '';
     };
+
     fullRepairInterval = mkOption {
       type = types.nullOr types.str;
       default = "3w";
-      example = literalExample "null";
+      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 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.
-        '';
+        Set to <literal>null</literal> to disable full repairs.
+      '';
     };
+
     fullRepairOptions = mkOption {
       type = types.listOf types.str;
-      default = [];
+      default = [ ];
       example = [ "--partitioner-range" ];
       description = ''
-          Options passed through to the full repair command.
-        '';
+        Options passed through to the full repair command.
+      '';
     };
+
     incrementalRepairInterval = mkOption {
       type = types.nullOr types.str;
       default = "3d";
-      example = literalExample "null";
+      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 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.
-        '';
+        Set to <literal>null</literal> to disable incremental repairs.
+      '';
     };
+
     incrementalRepairOptions = mkOption {
       type = types.listOf types.str;
-      default = [];
+      default = [ ];
       example = [ "--partitioner-range" ];
       description = ''
-          Options passed through to the incremental repair command.
-        '';
+        Options passed through to the incremental repair command.
+      '';
     };
+
     maxHeapSize = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -299,6 +348,7 @@ in {
         expensive GC will be (usually).
       '';
     };
+
     heapNewSize = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -322,6 +372,7 @@ in {
         100 MB per physical CPU core.
       '';
     };
+
     mallocArenaMax = mkOption {
       type = types.nullOr types.int;
       default = null;
@@ -330,6 +381,7 @@ in {
         Set this to control the amount of arenas per-thread in glibc.
       '';
     };
+
     remoteJmx = mkOption {
       type = types.bool;
       default = false;
@@ -341,6 +393,7 @@ in {
         See: https://wiki.apache.org/cassandra/JmxSecurity
       '';
     };
+
     jmxPort = mkOption {
       type = types.int;
       default = 7199;
@@ -351,8 +404,9 @@ in {
         Firewall it if needed.
       '';
     };
+
     jmxRoles = mkOption {
-      default = [];
+      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.
@@ -375,11 +429,13 @@ in {
         };
       });
     };
+
     jmxRolesFile = mkOption {
       type = types.nullOr types.path;
-      default = if (lib.versionAtLeast cfg.package.version "3.11")
-                then pkgs.writeText "jmx-roles-file" defaultJmxRolesFile
-                else null;
+      default =
+        if versionAtLeast cfg.package.version "3.11"
+        then pkgs.writeText "jmx-roles-file" defaultJmxRolesFile
+        else null;
       example = "/var/lib/cassandra/jmx.password";
       description = ''
         Specify your own jmx roles file.
@@ -391,102 +447,115 @@ in {
   };
 
   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.
-          '';
-        }
-      ];
+    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) {
-      extraUsers.${defaultUser} =
-        {  group = cfg.group;
-           home = cfg.homeDir;
-           createHome = true;
-           uid = config.ids.uids.cassandra;
-           description = "Cassandra service user";
-        };
-      extraGroups.${defaultUser}.gid = config.ids.gids.cassandra;
+      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 = {
+      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 =
-              lib.concatStringsSep " "
-                ([ "${cfg.package}/bin/nodetool" "repair" "--full"
-                 ] ++ cfg.fullRepairOptions);
-          };
+    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;
-          };
+        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 =
-              lib.concatStringsSep " "
-                ([ "${cfg.package}/bin/nodetool" "repair"
-                 ] ++ cfg.incrementalRepairOptions);
-          };
+    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;
-          };
+        timerConfig = {
+          OnBootSec = cfg.incrementalRepairInterval;
+          OnUnitActiveSec = cfg.incrementalRepairInterval;
+          Persistent = true;
+        };
       };
   };
+
+  meta.maintainers = with lib.maintainers; [ roberth ];
 }
diff --git a/nixos/modules/services/databases/couchdb.nix b/nixos/modules/services/databases/couchdb.nix
index c99a7529213..6cc29cd717e 100644
--- a/nixos/modules/services/databases/couchdb.nix
+++ b/nixos/modules/services/databases/couchdb.nix
@@ -4,24 +4,17 @@ with lib;
 
 let
   cfg = config.services.couchdb;
-  useVersion2 = strings.versionAtLeast (strings.getVersion cfg.package) "2.0";
   configFile = pkgs.writeText "couchdb.ini" (
     ''
       [couchdb]
       database_dir = ${cfg.databaseDir}
       uri_file = ${cfg.uriFile}
       view_index_dir = ${cfg.viewIndexDir}
-    '' + (if cfg.adminPass != null then
-    ''
+    '' + (optionalString (cfg.adminPass != null) ''
       [admins]
       ${cfg.adminUser} = ${cfg.adminPass}
-    '' else
-    "") + (if useVersion2 then
-    ''
+    '' + ''
       [chttpd]
-    '' else
-    ''
-      [httpd]
     '') +
     ''
       port = ${toString cfg.port}
@@ -30,8 +23,7 @@ let
       [log]
       file = ${cfg.logFile}
     '');
-  executable = if useVersion2 then "${cfg.package}/bin/couchdb"
-    else ''${cfg.package}/bin/couchdb -a ${configFile} -a ${pkgs.writeText "couchdb-extra.ini" cfg.extraConfig} -a ${cfg.configFile}'';
+  executable = "${cfg.package}/bin/couchdb";
 
 in {
 
@@ -177,8 +169,7 @@ in {
 
     environment.systemPackages = [ cfg.package ];
 
-    services.couchdb.configFile = mkDefault
-      (if useVersion2 then "/var/lib/couchdb/local.ini" else "/var/lib/couchdb/couchdb.ini");
+    services.couchdb.configFile = mkDefault "/var/lib/couchdb/local.ini";
 
     systemd.tmpfiles.rules = [
       "d '${dirOf cfg.uriFile}' - ${cfg.user} ${cfg.group} - -"
@@ -195,7 +186,7 @@ in {
         touch ${cfg.configFile}
       '';
 
-      environment = mkIf useVersion2 {
+      environment = {
         # we are actually specifying 4 configuration files:
         # 1. the preinstalled default.ini
         # 2. the module configuration
diff --git a/nixos/modules/services/databases/pgmanage.nix b/nixos/modules/services/databases/pgmanage.nix
index 0f8634dab31..8508e76b5cd 100644
--- a/nixos/modules/services/databases/pgmanage.nix
+++ b/nixos/modules/services/databases/pgmanage.nix
@@ -197,6 +197,7 @@ in {
         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
index f582b059277..fdc05312ece 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -18,7 +18,12 @@ let
     else toString value;
 
   # The main PostgreSQL configuration file.
-  configFile = pkgs.writeText "postgresql.conf" (concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${toStr v}") cfg.settings));
+  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";
 
@@ -53,6 +58,12 @@ in
         '';
       };
 
+      checkConfig = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Check the syntax of the configuration file at compile time";
+      };
+
       dataDir = mkOption {
         type = types.path;
         defaultText = "/var/lib/postgresql/\${config.services.postgresql.package.psqlSchema}";
@@ -152,7 +163,7 @@ in
               '';
               example = literalExample ''
                 {
-                  "DATABASE nextcloud" = "ALL PRIVILEGES";
+                  "DATABASE \"nextcloud\"" = "ALL PRIVILEGES";
                   "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES";
                 }
               '';
@@ -284,8 +295,7 @@ in
       # systems!
       mkDefault (if versionAtLeast config.system.stateVersion "20.03" then pkgs.postgresql_11
             else if versionAtLeast config.system.stateVersion "17.09" then pkgs.postgresql_9_6
-            else if versionAtLeast config.system.stateVersion "16.03" then pkgs.postgresql_9_5
-            else throw "postgresql_9_4 was removed, please upgrade your postgresql version.");
+            else throw "postgresql_9_5 was removed, please upgrade your postgresql version.");
 
     services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
 
@@ -314,6 +324,8 @@ in
      "/share/postgresql"
     ];
 
+    system.extraDependencies = lib.optional (cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) configFileCheck;
+
     systemd.services.postgresql =
       { description = "PostgreSQL Server";
 
@@ -337,7 +349,7 @@ in
               touch "${cfg.dataDir}/.first_startup"
             fi
 
-            ln -sfn "${configFile}" "${cfg.dataDir}/postgresql.conf"
+            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"
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index 117e6366225..7ec10c0eb5a 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -5,6 +5,8 @@ with lib;
 let
   cfg = config.services.redis;
 
+  ulimitNofile = cfg.maxclients + 32;
+
   mkValueString = value:
     if value == true then "yes"
     else if value == false then "no"
@@ -14,8 +16,8 @@ let
     listsAsDuplicateKeys = true;
     mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " ";
   } cfg.settings);
-in
-{
+
+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.")
@@ -88,6 +90,13 @@ in
         example = "/run/redis/redis.sock";
       };
 
+      unixSocketPerm = mkOption {
+        type = types.int;
+        default = 750;
+        description = "Change permissions for the socket";
+        example = 700;
+      };
+
       logLevel = mkOption {
         type = types.str;
         default = "notice"; # debug, verbose, notice, warning
@@ -114,6 +123,12 @@ in
         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] ];
@@ -204,7 +219,6 @@ in
         '';
         example = literalExample ''
           {
-            unixsocketperm = "700";
             loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
           }
         '';
@@ -247,6 +261,7 @@ in
         logfile = cfg.logfile;
         syslog-enabled = cfg.syslog;
         databases = cfg.databases;
+        maxclients = cfg.maxclients;
         save = map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") cfg.save;
         dbfilename = "dump.rdb";
         dir = "/var/lib/redis";
@@ -256,7 +271,7 @@ in
         slowlog-max-len = cfg.slowLogMaxLen;
       }
       (mkIf (cfg.bind != null) { bind = cfg.bind; })
-      (mkIf (cfg.unixSocket != null) { unixsocket = cfg.unixSocket; })
+      (mkIf (cfg.unixSocket != null) { unixsocket = cfg.unixSocket; unixsocketperm = "${toString cfg.unixSocketPerm}"; })
       (mkIf (cfg.slaveOf != null) { slaveof = "${cfg.slaveOf.ip} ${cfg.slaveOf.port}"; })
       (mkIf (cfg.masterAuth != null) { masterauth = cfg.masterAuth; })
       (mkIf (cfg.requirePass != null) { requirepass = cfg.requirePass; })
@@ -277,11 +292,46 @@ in
 
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/redis-server /run/redis/redis.conf";
-        RuntimeDirectory = "redis";
-        StateDirectory = "redis";
         Type = "notify";
+        # User and group
         User = "redis";
         Group = "redis";
+        # Runtime directory and mode
+        RuntimeDirectory = "redis";
+        RuntimeDirectoryMode = "0750";
+        # State directory and mode
+        StateDirectory = "redis";
+        StateDirectoryMode = "0700";
+        # Access write directories
+        UMask = "0077";
+        # Capabilities
+        CapabilityBoundingSet = "";
+        # Security
+        NoNewPrivileges = true;
+        # Process Properties
+        LimitNOFILE = "${toString ulimitNofile}";
+        # 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" ];
+        RestrictNamespaces = true;
+        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 @privileged @raw-io @reboot @resources @setuid @swap";
       };
     };
   };
diff --git a/nixos/modules/services/desktops/geoclue2.nix b/nixos/modules/services/desktops/geoclue2.nix
index 6702bd395a0..0dc0643afbc 100644
--- a/nixos/modules/services/desktops/geoclue2.nix
+++ b/nixos/modules/services/desktops/geoclue2.nix
@@ -188,7 +188,8 @@ in
 
     systemd.packages = [ package ];
 
-    # we cannot use DynamicUser as we need the the geoclue user to exist for the dbus policy to work
+    # we cannot use DynamicUser as we need the the geoclue user to exist for the
+    # dbus policy to work
     users = {
       users.geoclue = {
         isSystemUser = true;
@@ -217,6 +218,7 @@ in
         # 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";
diff --git a/nixos/modules/services/desktops/pipewire/alsa-monitor.conf.json b/nixos/modules/services/desktops/pipewire/alsa-monitor.conf.json
new file mode 100644
index 00000000000..53fc9cc9634
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/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/bluez-monitor.conf.json b/nixos/modules/services/desktops/pipewire/bluez-monitor.conf.json
new file mode 100644
index 00000000000..6d1c23e8256
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/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/client-rt.conf.json b/nixos/modules/services/desktops/pipewire/client-rt.conf.json
new file mode 100644
index 00000000000..284d8c394a6
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/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-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"
+    }
+  ],
+  "filter.properties": {},
+  "stream.properties": {}
+}
diff --git a/nixos/modules/services/desktops/pipewire/client.conf.json b/nixos/modules/services/desktops/pipewire/client.conf.json
new file mode 100644
index 00000000000..71294a0e78a
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/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/jack.conf.json b/nixos/modules/services/desktops/pipewire/jack.conf.json
new file mode 100644
index 00000000000..a6bd3491785
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/jack.conf.json
@@ -0,0 +1,28 @@
+{
+  "context.properties": {
+    "log.level": 0
+  },
+  "context.spa-libs": {
+    "support.*": "support/libspa-support"
+  },
+  "context.modules": [
+    {
+      "name": "libpipewire-module-rtkit",
+      "args": {},
+      "flags": [
+        "ifexists",
+        "nofail"
+      ]
+    },
+    {
+      "name": "libpipewire-module-protocol-native"
+    },
+    {
+      "name": "libpipewire-module-client-node"
+    },
+    {
+      "name": "libpipewire-module-metadata"
+    }
+  ],
+  "jack.properties": {}
+}
diff --git a/nixos/modules/services/desktops/pipewire/media-session.conf.json b/nixos/modules/services/desktops/pipewire/media-session.conf.json
new file mode 100644
index 00000000000..24906e767d6
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/media-session.conf.json
@@ -0,0 +1,67 @@
+{
+  "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",
+      "logind",
+      "restore-stream",
+      "streams-follow-default"
+    ]
+  }
+}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix b/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
index 81f4762e1e6..17a2d49bb1f 100644
--- a/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
+++ b/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
@@ -4,28 +4,27 @@
 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;
 
-  # Helpers for generating the pipewire JSON config file
-  mkSPAValueString = v:
-  if builtins.isList v then "[${lib.concatMapStringsSep " " mkSPAValueString v}]"
-  else if lib.types.attrs.check v then
-    "{${lib.concatStringsSep " " (mkSPAKeyValue v)}}"
-  else lib.generators.mkValueStringDefault { } v;
-
-  mkSPAKeyValue = attrs: map (def: def.content) (
-  lib.sortProperties
-    (
-      lib.mapAttrsToList
-        (k: v: lib.mkOrder (v._priority or 1000) "${lib.escape [ "=" ] k} = ${mkSPAValueString (v._content or v)}")
-        attrs
-    )
-  );
+  # 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 = (builtins.fromJSON (builtins.readFile ./alsa-monitor.conf.json));
+    bluez-monitor = (builtins.fromJSON (builtins.readFile ./bluez-monitor.conf.json));
+    media-session = (builtins.fromJSON (builtins.readFile ./media-session.conf.json));
+    v4l2-monitor = (builtins.fromJSON (builtins.readFile ./v4l2-monitor.conf.json));
+  };
 
-  toSPAJSON = attrs: lib.concatStringsSep "\n" (mkSPAKeyValue attrs);
+  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 = {
@@ -51,272 +50,41 @@ in {
         '';
       };
 
-      config = mkOption {
-        type = types.attrs;
-        description = ''
-          Configuration for the media session core.
-        '';
-        default = {
-          # media-session config file
-          properties = {
-            # Properties to configure the session and some
-            # modules
-            #mem.mlock-all = false;
-            #context.profile.modules = "default,rtkit";
-          };
-
-          spa-libs = {
-            # Mapping from factory name to library.
-            "api.bluez5.*" = "bluez5/libspa-bluez5";
-            "api.alsa.*" = "alsa/libspa-alsa";
-            "api.v4l2.*" = "v4l2/libspa-v4l2";
-            "api.libcamera.*" = "libcamera/libspa-libcamera";
-          };
-
-          modules = {
-            # These are the modules that are enabled when a file with
-            # the key name is found in the media-session.d config directory.
-            # the default bundle is always enabled.
-
-            default = [
-              "flatpak"			# manages flatpak access
-              "portal"			# manage portal permissions
-              "v4l2"			# video for linux udev detection
-              #"libcamera"		# libcamera udev detection
-              "suspend-node"		# suspend inactive nodes
-              "policy-node"		# configure and link nodes
-              #"metadata"		# export metadata API
-              #"default-nodes"		# restore default nodes
-              #"default-profile"	# restore default profiles
-              #"default-routes"		# restore default route
-              #"streams-follow-default"	# move streams when default changes
-              #"alsa-seq"		# alsa seq midi support
-              #"alsa-monitor"		# alsa udev detection
-              #"bluez5"			# bluetooth support
-              #"restore-stream"		# restore stream settings
-            ];
-            "with-audio" = [
-              "metadata"
-              "default-nodes"
-              "default-profile"
-              "default-routes"
-              "alsa-seq"
-              "alsa-monitor"
-            ];
-            "with-alsa" = [
-              "with-audio"
-            ];
-            "with-jack" = [
-              "with-audio"
-            ];
-            "with-pulseaudio" = [
-              "with-audio"
-              "bluez5"
-              "restore-stream"
-              "streams-follow-default"
-            ];
-          };
+      config = {
+        media-session = mkOption {
+          type = json.type;
+          description = ''
+            Configuration for the media session core. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/media-session.d/media-session.conf
+          '';
+          default = {};
         };
-      };
-
-      alsaMonitorConfig = mkOption {
-        type = types.attrs;
-        description = ''
-          Configuration for the alsa monitor.
-        '';
-        default = {
-          # alsa-monitor config file
-          properties = {
-            #alsa.jack-device = true
-          };
 
-          rules = [
-          # an array of matches/actions to evaluate
-          {
-            # rules for matching a device or node. It is an array of
-            # properties that all need to match the regexp. If any of the
-            # matches work, the actions are executed for the object.
-            matches = [
-              {
-                # this matches all cards
-                device.name = "~alsa_card.*";
-              }
-            ];
-            actions = {
-              # actions can update properties on the matched object.
-              update-props = {
-                api.alsa.use-acp = true;
-                #api.alsa.use-ucm = true;
-                #api.alsa.soft-mixer = false;
-                #api.alsa.ignore-dB = false;
-                #device.profile-set = "profileset-name";
-                #device.profile = "default profile name";
-                api.acp.auto-profile = false;
-                api.acp.auto-port = false;
-                #device.nick = "My Device";
-              };
-            };
-          }
-          {
-            matches = [
-              {
-                # matches all sinks
-                node.name = "~alsa_input.*";
-              }
-              {
-                # matches all sources
-                node.name = "~alsa_output.*";
-              }
-            ];
-            actions = {
-              update-props = {
-                #node.nick = 			"My Node";
-                #node.nick = 			null;
-                #priority.driver = 		100;
-                #priority.session = 		100;
-                #node.pause-on-idle = 		false;
-                #resample.quality = 		4;
-                #channelmix.normalize =		false;
-                #channelmix.mix-lfe = 		false;
-                #audio.channels = 		2;
-                #audio.format = 		"S16LE";
-                #audio.rate = 			44100;
-                #audio.position = 		"FL,FR";
-                #api.alsa.period-size =         1024;
-                #api.alsa.headroom =            0;
-                #api.alsa.disable-mmap =        false;
-                #api.alsa.disable-batch =       false;
-              };
-            };
-          }
-          ];
+        alsa-monitor = mkOption {
+          type = json.type;
+          description = ''
+            Configuration for the alsa monitor. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/media-session.d/alsa-monitor.conf
+          '';
+          default = {};
         };
-      };
 
-      bluezMonitorConfig = mkOption {
-        type = types.attrs;
-        description = ''
-          Configuration for the bluez5 monitor.
-        '';
-        default = {
-          # bluez-monitor config file
-          properties = {
-            # msbc is not expected to work on all headset + adapter combinations.
-            #bluez5.msbc-support = true;
-            #bluez5.sbc-xq-support = true;
-
-            # Enabled headset roles (default: [ hsp_hs hfp_ag ]), this
-            # property only applies to native backend. Currently some headsets
-            # (Sony WH-1000XM3) are not working with both hsp_ag and hfp_ag
-            # enabled, disable either hsp_ag or hfp_ag to work around it.
-            #
-            # Supported headset roles: hsp_hs (HSP Headset),
-            #                          hsp_ag (HSP Audio Gateway),
-            #                          hfp_ag (HFP Audio Gateway)
-            #bluez5.headset-roles = [ "hsp_hs" "hsp_ag" "hfp_ag" ];
-
-            # Enabled A2DP codecs (default: all)
-            #bluez5.codecs = [ "sbc" "aac" "ldac" "aptx" "aptx_hd" ];
-          };
-
-          rules = [
-          # an array of matches/actions to evaluate
-          {
-            # rules for matching a device or node. It is an array of
-            # properties that all need to match the regexp. If any of the
-            # matches work, the actions are executed for the object.
-            matches = [
-              {
-                # this matches all cards
-                device.name = "~bluez_card.*";
-              }
-            ];
-            actions = {
-              # actions can update properties on the matched object.
-              update-props = {
-                #device.nick = 			"My Device";
-              };
-            };
-          }
-          {
-            matches = [
-              {
-                # matches all sinks
-                node.name = "~bluez_input.*";
-              }
-              {
-                # matches all sources
-                node.name = "~bluez_output.*";
-              }
-            ];
-            actions = {
-              update-props = {
-                #node.nick = 			"My Node"
-                #node.nick = 			null;
-                #priority.driver = 		100;
-                #priority.session = 		100;
-                #node.pause-on-idle = 		false;
-                #resample.quality = 		4;
-                #channelmix.normalize =		false;
-                #channelmix.mix-lfe = 		false;
-              };
-            };
-          }
-          ];
+        bluez-monitor = mkOption {
+          type = json.type;
+          description = ''
+            Configuration for the bluez5 monitor. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/media-session.d/bluez-monitor.conf
+          '';
+          default = {};
         };
-      };
-
-      v4l2MonitorConfig = mkOption {
-        type = types.attrs;
-        description = ''
-          Configuration for the V4L2 monitor.
-        '';
-        default = {
-          # v4l2-monitor config file
-          properties = {
-          };
 
-          rules = [
-            # an array of matches/actions to evaluate
-            {
-              # rules for matching a device or node. It is an array of
-              # properties that all need to match the regexp. If any of the
-              # matches work, the actions are executed for the object.
-              matches = [
-                {
-                  # this matches all devices
-                  device.name = "~v4l2_device.*";
-                }
-              ];
-              actions = {
-                # actions can update properties on the matched object.
-                update-props = {
-                  #device.nick = 			"My Device";
-                };
-              };
-            }
-            {
-              matches = [
-                {
-                  # matches all sinks
-                  node.name = "~v4l2_input.*";
-                }
-                {
-                  # matches all sources
-                  node.name = "~v4l2_output.*";
-                }
-              ];
-              actions = {
-                update-props = {
-                  #node.nick = 			"My Node";
-                  #node.nick = 			null;
-                  #priority.driver = 		100;
-                  #priority.session = 		100;
-                  #node.pause-on-idle = 		true;
-                };
-              };
-            }
-          ];
+        v4l2-monitor = mkOption {
+          type = json.type;
+          description = ''
+            Configuration for the V4L2 monitor. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/media-session.d/v4l2-monitor.conf
+          '';
+          default = {};
         };
       };
     };
@@ -325,17 +93,37 @@ in {
   ###### implementation
   config = mkIf cfg.enable {
     environment.systemPackages = [ cfg.package ];
-    services.pipewire.sessionManagerExecutable = "${cfg.package}/bin/pipewire-media-session";
+    systemd.packages = [ cfg.package ];
+    systemd.user.services.pipewire-media-session.wantedBy = [ "pipewire.service" ];
 
-    environment.etc."pipewire/media-session.d/media-session.conf" = { text = toSPAJSON cfg.config; };
-    environment.etc."pipewire/media-session.d/v4l2-monitor.conf" = { text = toSPAJSON cfg.v4l2MonitorConfig; };
+    environment.etc."pipewire/media-session.d/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 { text = toSPAJSON cfg.alsaMonitorConfig; };
+    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 { text = toSPAJSON cfg.bluezMonitorConfig; };
+    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 = ""; };
+    environment.etc."pipewire/media-session.d/with-jack" =
+      mkIf config.services.pipewire.jack.enable {
+        text = "";
+      };
   };
 }
diff --git a/nixos/modules/services/desktops/pipewire/pipewire-pulse.conf.json b/nixos/modules/services/desktops/pipewire/pipewire-pulse.conf.json
new file mode 100644
index 00000000000..17bbbdef117
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/pipewire-pulse.conf.json
@@ -0,0 +1,41 @@
+{
+  "context.properties": {},
+  "context.spa-libs": {
+    "audio.convert.*": "audioconvert/libspa-audioconvert",
+    "support.*": "support/libspa-support"
+  },
+  "context.modules": [
+    {
+      "name": "libpipewire-module-rtkit",
+      "args": {},
+      "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"
+        }
+      }
+    }
+  ],
+  "stream.properties": {}
+}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire.conf.json b/nixos/modules/services/desktops/pipewire/pipewire.conf.json
new file mode 100644
index 00000000000..a9330f54f4f
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/pipewire.conf.json
@@ -0,0 +1,82 @@
+{
+  "context.properties": {
+    "link.max-buffers": 16,
+    "core.daemon": true,
+    "core.name": "pipewire-0",
+    "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-rtkit",
+      "args": {},
+      "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",
+        "priority.driver": 8000
+      }
+    }
+  ],
+  "context.exec": []
+}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire.nix b/nixos/modules/services/desktops/pipewire/pipewire.nix
index 044120de7c7..dbd6c5d87e1 100644
--- a/nixos/modules/services/desktops/pipewire/pipewire.nix
+++ b/nixos/modules/services/desktops/pipewire/pipewire.nix
@@ -4,6 +4,7 @@
 with lib;
 
 let
+  json = pkgs.formats.json {};
   cfg = config.services.pipewire;
   enable32BitAlsaPlugins = cfg.alsa.support32Bit
                            && pkgs.stdenv.isx86_64
@@ -18,23 +19,24 @@ let
     ln -s "${cfg.package.jack}/lib" "$out/lib/pipewire"
   '';
 
-  # Helpers for generating the pipewire JSON config file
-  mkSPAValueString = v:
-  if builtins.isList v then "[${lib.concatMapStringsSep " " mkSPAValueString v}]"
-  else if lib.types.attrs.check v then
-    "{${lib.concatStringsSep " " (mkSPAKeyValue v)}}"
-  else lib.generators.mkValueStringDefault { } v;
-
-  mkSPAKeyValue = attrs: map (def: def.content) (
-  lib.sortProperties
-    (
-      lib.mapAttrsToList
-        (k: v: lib.mkOrder (v._priority or 1000) "${lib.escape [ "=" ] k} = ${mkSPAValueString (v._content or v)}")
-        attrs
-    )
-  );
+  # 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 = builtins.fromJSON (builtins.readFile ./client.conf.json);
+    client-rt = builtins.fromJSON (builtins.readFile ./client-rt.conf.json);
+    jack = builtins.fromJSON (builtins.readFile ./jack.conf.json);
+    # Remove session manager invocation from the upstream generated file, it points to the wrong path
+    pipewire = builtins.fromJSON (builtins.readFile ./pipewire.conf.json);
+    pipewire-pulse = builtins.fromJSON (builtins.readFile ./pipewire-pulse.conf.json);
+  };
 
-  toSPAJSON = attrs: lib.concatStringsSep "\n" (mkSPAKeyValue attrs);
+  configs = {
+    client = recursiveUpdate defaults.client cfg.config.client;
+    client-rt = recursiveUpdate defaults.client-rt cfg.config.client-rt;
+    jack = recursiveUpdate defaults.jack cfg.config.jack;
+    pipewire = recursiveUpdate defaults.pipewire cfg.config.pipewire;
+    pipewire-pulse = recursiveUpdate defaults.pipewire-pulse cfg.config.pipewire-pulse;
+  };
 in {
 
   meta = {
@@ -64,129 +66,51 @@ in {
         '';
       };
 
-      config = mkOption {
-        type = types.attrs;
-        description = ''
-          Configuration for the pipewire daemon.
-        '';
-        default = {
-          properties = {
-            ## set-prop is used to configure properties in the system
-            #
-            # "library.name.system" = "support/libspa-support";
-            # "context.data-loop.library.name.system" = "support/libspa-support";
-            "link.max-buffers" = 16; # version < 3 clients can't handle more than 16
-            #"mem.allow-mlock" = false;
-            #"mem.mlock-all" = true;
-            ## https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/src/pipewire/pipewire.h#L93
-            #"log.level" = 2; # 5 is trace, which is verbose as hell, default is 2 which is warnings, 4 is debug output, 3 is info
-
-            ## Properties for the DSP configuration
-            #
-            #"default.clock.rate" = 48000;
-            #"default.clock.quantum" = 1024;
-            #"default.clock.min-quantum" = 32;
-            #"default.clock.max-quantum" = 8192;
-            #"default.video.width" = 640;
-            #"default.video.height" = 480;
-            #"default.video.rate.num" = 25;
-            #"default.video.rate.denom" = 1;
-          };
-
-          spa-libs = {
-            ## add-spa-lib <factory-name regex> <library-name>
-            #
-            # used to find spa factory names. It maps an spa factory name
-            # regular expression to a library name that should contain
-            # that factory.
-            #
-            "audio.convert*" = "audioconvert/libspa-audioconvert";
-            "api.alsa.*" = "alsa/libspa-alsa";
-            "api.v4l2.*" = "v4l2/libspa-v4l2";
-            "api.libcamera.*" = "libcamera/libspa-libcamera";
-            "api.bluez5.*" = "bluez5/libspa-bluez5";
-            "api.vulkan.*" = "vulkan/libspa-vulkan";
-            "api.jack.*" = "jack/libspa-jack";
-            "support.*" = "support/libspa-support";
-            # "videotestsrc" = "videotestsrc/libspa-videotestsrc";
-            # "audiotestsrc" = "audiotestsrc/libspa-audiotestsrc";
-          };
-
-          modules = {
-            ##  <module-name> = { [args = "<key>=<value> ..."]
-            #                     [flags = ifexists] }
-            #                     [flags = [ifexists]|[nofail]}
-            #
-            # Loads a module with the given parameters.
-            # If ifexists is given, the module is ignoed when it is not found.
-            # If nofail is given, module initialization failures are ignored.
-            #
-            libpipewire-module-rtkit = {
-              args = {
-                #rt.prio = 20;
-                #rt.time.soft = 200000;
-                #rt.time.hard = 200000;
-                #nice.level = -11;
-              };
-              flags = "ifexists|nofail";
-            };
-            libpipewire-module-protocol-native = { _priority = -100; _content = "null"; };
-            libpipewire-module-profiler = "null";
-            libpipewire-module-metadata = "null";
-            libpipewire-module-spa-device-factory = "null";
-            libpipewire-module-spa-node-factory = "null";
-            libpipewire-module-client-node = "null";
-            libpipewire-module-client-device = "null";
-            libpipewire-module-portal = "null";
-            libpipewire-module-access = {
-              args.access = {
-                allowed = ["${builtins.unsafeDiscardStringContext cfg.sessionManagerExecutable}"];
-                rejected = [];
-                restricted = [];
-                force = "flatpak";
-              };
-            };
-            libpipewire-module-adapter = "null";
-            libpipewire-module-link-factory = "null";
-            libpipewire-module-session-manager = "null";
-          };
-
-          objects = {
-            ## create-object [-nofail] <factory-name> [<key>=<value> ...]
-            #
-            # Creates an object from a PipeWire factory with the given parameters.
-            # If -nofail is given, errors are ignored (and no object is created)
-            #
-          };
+      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
+          '';
+        };
 
-          exec = {
-            ## exec <program-name>
-            #
-            # Execute the given program. This is usually used to start the
-            # session manager. run the session manager with -h for options
-            #
-            "${builtins.unsafeDiscardStringContext cfg.sessionManagerExecutable}" = { args = "\"${lib.concatStringsSep " " cfg.sessionManagerArguments}\""; };
-          };
+        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
+          '';
         };
-      };
 
-      sessionManagerExecutable = mkOption {
-        type = types.str;
-        default = "";
-        example = literalExample ''${pkgs.pipewire.mediaSession}/bin/pipewire-media-session'';
-        description = ''
-          Path to the session manager executable.
-        '';
-      };
+        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
+          '';
+        };
 
-      sessionManagerArguments = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        example = literalExample ''["-p" "bluez5.msbc-support=true"]'';
-        description = ''
-          Arguments passed to the pipewire session manager.
-        '';
+        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 = {
@@ -253,13 +177,26 @@ in {
       source = "${cfg.package}/share/alsa/alsa.conf.d/99-pipewire-default.conf";
     };
 
+    environment.etc."pipewire/client.conf" = {
+      source = json.generate "client.conf" configs.client;
+    };
+    environment.etc."pipewire/client-rt.conf" = {
+      source = json.generate "client-rt.conf" configs.client-rt;
+    };
+    environment.etc."pipewire/jack.conf" = {
+      source = json.generate "jack.conf" configs.jack;
+    };
+    environment.etc."pipewire/pipewire.conf" = {
+      source = json.generate "pipewire.conf" configs.pipewire;
+    };
+    environment.etc."pipewire/pipewire-pulse.conf" = {
+      source = json.generate "pipewire-pulse.conf" configs.pipewire-pulse;
+    };
+
     environment.sessionVariables.LD_LIBRARY_PATH =
       lib.optional cfg.jack.enable "/run/current-system/sw/lib/pipewire";
 
     # https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/464#note_723554
-    systemd.user.services.pipewire.environment = {
-      "PIPEWIRE_LINK_PASSIVE" = "1";
-      "PIPEWIRE_CONFIG_FILE" = pkgs.writeText "pipewire.conf" (toSPAJSON cfg.config);
-    };
+    systemd.user.services.pipewire.environment."PIPEWIRE_LINK_PASSIVE" = "1";
   };
 }
diff --git a/nixos/modules/services/desktops/pipewire/v4l2-monitor.conf.json b/nixos/modules/services/desktops/pipewire/v4l2-monitor.conf.json
new file mode 100644
index 00000000000..b08cba1b604
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/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/display-managers/greetd.nix b/nixos/modules/services/display-managers/greetd.nix
new file mode 100644
index 00000000000..c3072bf0996
--- /dev/null
+++ b/nixos/modules/services/display-managers/greetd.nix
@@ -0,0 +1,106 @@
+{ 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 = "pkgs.greetd.greetd";
+      description = "The greetd package that should be used.";
+    };
+
+    settings = mkOption {
+      type = settingsFormat.type;
+      example = literalExample ''
+        {
+          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 = "!(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;
+  };
+
+  meta.maintainers = with maintainers; [ queezle ];
+}
diff --git a/nixos/modules/services/games/factorio.nix b/nixos/modules/services/games/factorio.nix
index 73099ae3363..a1aa5739d06 100644
--- a/nixos/modules/services/games/factorio.nix
+++ b/nixos/modules/services/games/factorio.nix
@@ -36,6 +36,7 @@ let
     only_admins_can_pause_the_game = true;
     autosave_only_on_server = true;
     admins = [];
+    non_blocking_saving = cfg.nonBlockingSaving;
   } // cfg.extraSettings;
   serverSettingsFile = pkgs.writeText "server-settings.json" (builtins.toJSON (filterAttrsRecursive (n: v: v != null) serverSettings));
   modDir = pkgs.factorio-utils.mkModDirDrv cfg.mods;
@@ -193,6 +194,15 @@ in
           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.
+        '';
+      };
     };
   };
 
diff --git a/nixos/modules/services/games/minetest-server.nix b/nixos/modules/services/games/minetest-server.nix
index f52079fc1ef..2111c970d4f 100644
--- a/nixos/modules/services/games/minetest-server.nix
+++ b/nixos/modules/services/games/minetest-server.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg   = config.services.minetest-server;
-  flag  = val: name: if val != null then "--${name} ${val} " else "";
+  flag  = val: name: if val != null then "--${name} ${toString val} " else "";
   flags = [
     (flag cfg.gameId "gameid")
     (flag cfg.world "world")
diff --git a/nixos/modules/services/games/quake3-server.nix b/nixos/modules/services/games/quake3-server.nix
new file mode 100644
index 00000000000..1dc01260e8f
--- /dev/null
+++ b/nixos/modules/services/games/quake3-server.nix
@@ -0,0 +1,111 @@
+{ 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;
+        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/hardware/acpid.nix b/nixos/modules/services/hardware/acpid.nix
index 4c97485d972..3e619fe32ef 100644
--- a/nixos/modules/services/hardware/acpid.nix
+++ b/nixos/modules/services/hardware/acpid.nix
@@ -3,21 +3,22 @@
 with lib;
 
 let
+  cfg = config.services.acpid;
 
   canonicalHandlers = {
     powerEvent = {
       event = "button/power.*";
-      action = config.services.acpid.powerEventCommands;
+      action = cfg.powerEventCommands;
     };
 
     lidEvent = {
       event = "button/lid.*";
-      action = config.services.acpid.lidEventCommands;
+      action = cfg.lidEventCommands;
     };
 
     acEvent = {
       event = "ac_adapter.*";
-      action = config.services.acpid.acEventCommands;
+      action = cfg.acEventCommands;
     };
   };
 
@@ -33,7 +34,7 @@ let
             echo "event=${handler.event}" > $fn
             echo "action=${pkgs.writeShellScriptBin "${name}.sh" handler.action }/bin/${name}.sh '%e'" >> $fn
           '';
-        in concatStringsSep "\n" (mapAttrsToList f (canonicalHandlers // config.services.acpid.handlers))
+        in concatStringsSep "\n" (mapAttrsToList f (canonicalHandlers // cfg.handlers))
       }
     '';
 
@@ -47,11 +48,7 @@ in
 
     services.acpid = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Whether to enable the ACPI daemon.";
-      };
+      enable = mkEnableOption "the ACPI daemon";
 
       logEvents = mkOption {
         type = types.bool;
@@ -129,26 +126,28 @@ in
 
   ###### implementation
 
-  config = mkIf config.services.acpid.enable {
+  config = mkIf cfg.enable {
 
     systemd.services.acpid = {
       description = "ACPI Daemon";
+      documentation = [ "man:acpid(8)" ];
 
       wantedBy = [ "multi-user.target" ];
-      after = [ "systemd-udev-settle.service" ];
-
-      path = [ pkgs.acpid ];
 
       serviceConfig = {
-        Type = "forking";
+        ExecStart = escapeShellArgs
+          ([ "${pkgs.acpid}/bin/acpid"
+             "--foreground"
+             "--netlink"
+             "--confdir" "${acpiConfDir}"
+           ] ++ optional cfg.logEvents "--logevents"
+          );
       };
-
       unitConfig = {
         ConditionVirtualization = "!systemd-nspawn";
         ConditionPathExists = [ "/proc/acpi" ];
       };
 
-      script = "acpid ${optionalString config.services.acpid.logEvents "--logevents"} --confdir ${acpiConfDir}";
     };
 
   };
diff --git a/nixos/modules/services/hardware/fancontrol.nix b/nixos/modules/services/hardware/fancontrol.nix
index e1ce11a5aef..3722db5bc51 100644
--- a/nixos/modules/services/hardware/fancontrol.nix
+++ b/nixos/modules/services/hardware/fancontrol.nix
@@ -6,21 +6,21 @@ let
   cfg = config.hardware.fancontrol;
   configFile = pkgs.writeText "fancontrol.conf" cfg.config;
 
-in{
+in
+{
   options.hardware.fancontrol = {
     enable = mkEnableOption "software fan control (requires fancontrol.config)";
 
     config = mkOption {
-      default = null;
-      type = types.nullOr types.lines;
-      description = "Fancontrol configuration file content. See <citerefentry><refentrytitle>pwmconfig</refentrytitle><manvolnum>8</manvolnum></citerefentry> from the lm_sensors package.";
+      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
+        FCFANS=hwmon4/device/pwm1=hwmon4/device/fan1_input
         MINTEMP=hwmon4/device/pwm1=35
         MAXTEMP=hwmon4/device/pwm1=65
         MINSTART=hwmon4/device/pwm1=150
@@ -30,16 +30,30 @@ in{
   };
 
   config = mkIf cfg.enable {
+
+    users = {
+      groups.lm_sensors = {};
+
+      users.fancontrol = {
+        isSystemUser = true;
+        group = "lm_sensors";
+        description = "fan speed controller";
+      };
+    };
+
     systemd.services.fancontrol = {
-      unitConfig.Documentation = "man:fancontrol(8)";
+      documentation = [ "man:fancontrol(8)" ];
       description = "software fan control";
       wantedBy = [ "multi-user.target" ];
       after = [ "lm_sensors.service" ];
 
       serviceConfig = {
-        Type = "simple";
         ExecStart = "${pkgs.lm_sensors}/sbin/fancontrol ${configFile}";
+        Group = "lm_sensors";
+        User = "fancontrol";
       };
     };
   };
+
+  meta.maintainers = [ maintainers.evils ];
 }
diff --git a/nixos/modules/services/hardware/pcscd.nix b/nixos/modules/services/hardware/pcscd.nix
index f3fc4c3cc79..4fc1e351f50 100644
--- a/nixos/modules/services/hardware/pcscd.nix
+++ b/nixos/modules/services/hardware/pcscd.nix
@@ -10,39 +10,37 @@ let
     paths = map (p: "${p}/pcsc/drivers") config.services.pcscd.plugins;
   };
 
-in {
+in
+{
 
   ###### interface
 
-  options = {
-
-    services.pcscd = {
-      enable = mkEnableOption "PCSC-Lite daemon";
-
-      plugins = mkOption {
-        type = types.listOf types.package;
-        default = [ pkgs.ccid ];
-        defaultText = "[ pkgs.ccid ]";
-        example = literalExample "[ 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.
-        '';
-      };
+  options.services.pcscd = {
+    enable = mkEnableOption "PCSC-Lite daemon";
+
+    plugins = mkOption {
+      type = types.listOf types.package;
+      default = [ pkgs.ccid ];
+      defaultText = "[ pkgs.ccid ]";
+      example = literalExample "[ 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.
+      '';
     };
   };
 
@@ -50,20 +48,26 @@ in {
 
   config = mkIf config.services.pcscd.enable {
 
-    systemd.sockets.pcscd = {
-      description = "PCSC-Lite Socket";
-      wantedBy = [ "sockets.target" ];
-      before = [ "multi-user.target" ];
-      socketConfig.ListenStream = "/run/pcscd/pcscd.comm";
-    };
+    environment.etc."reader.conf".source = cfgFile;
+
+    environment.systemPackages = [ pkgs.pcsclite ];
+    systemd.packages = [ (getBin pkgs.pcsclite) ];
+
+    systemd.sockets.pcscd.wantedBy = [ "sockets.target" ];
 
     systemd.services.pcscd = {
-      description = "PCSC-Lite daemon";
       environment.PCSCLITE_HP_DROPDIR = pluginEnv;
-      serviceConfig = {
-        ExecStart = "${getBin pkgs.pcsclite}/sbin/pcscd -f -x -c ${cfgFile}";
-        ExecReload = "${getBin pkgs.pcsclite}/sbin/pcscd -H";
-      };
+      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/sane.nix b/nixos/modules/services/hardware/sane.nix
index 03070a8f9e7..8c1bde7b415 100644
--- a/nixos/modules/services/hardware/sane.nix
+++ b/nixos/modules/services/hardware/sane.nix
@@ -4,9 +4,7 @@ with lib;
 
 let
 
-  pkg = if config.hardware.sane.snapshot
-    then pkgs.sane-backends-git
-    else pkgs.sane-backends;
+  pkg = pkgs.sane-backends;
 
   sanedConf = pkgs.writeTextFile {
     name = "saned.conf";
@@ -32,7 +30,7 @@ let
   };
 
   backends = [ pkg netConf ] ++ optional config.services.saned.enable sanedConf ++ config.hardware.sane.extraBackends;
-  saneConfig = pkgs.mkSaneConfig { paths = backends; };
+  saneConfig = pkgs.mkSaneConfig { paths = backends; inherit (config.hardware.sane) disabledDefaultBackends; };
 
   enabled = config.hardware.sane.enable || config.services.saned.enable;
 
@@ -75,6 +73,16 @@ in
       example = literalExample "[ 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;
@@ -155,6 +163,7 @@ in
       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/spacenavd.nix b/nixos/modules/services/hardware/spacenavd.nix
index 7afae76cc4f..cecc4d6f029 100644
--- a/nixos/modules/services/hardware/spacenavd.nix
+++ b/nixos/modules/services/hardware/spacenavd.nix
@@ -13,7 +13,7 @@ in {
   };
 
   config = mkIf cfg.enable {
-    systemd.user.services.spacenavd = {
+    systemd.services.spacenavd = {
       description = "Daemon for the Spacenavigator 6DOF mice by 3Dconnexion";
       after = [ "syslog.target" ];
       wantedBy = [ "graphical.target" ];
diff --git a/nixos/modules/services/hardware/tcsd.nix b/nixos/modules/services/hardware/tcsd.nix
index 68cb5d791aa..0d36bce357b 100644
--- a/nixos/modules/services/hardware/tcsd.nix
+++ b/nixos/modules/services/hardware/tcsd.nix
@@ -119,22 +119,31 @@ in
 
     environment.systemPackages = [ pkgs.trousers ];
 
-#    system.activationScripts.tcsd =
-#      ''
-#        chown ${cfg.user}:${cfg.group} ${tcsdConf}
-#      '';
+    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 = "TCSD";
-      after = [ "systemd-udev-settle.service" ];
+      description = "Manager for Trusted Computing resources";
+      documentation = [ "man:tcsd(8)" ];
+
+      requires = [ "dev-tpm0.device" ];
+      after = [ "dev-tpm0.device" ];
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.trousers ];
-      preStart =
-        ''
-        mkdir -m 0700 -p ${cfg.stateDir}
-        chown -R ${cfg.user}:${cfg.group} ${cfg.stateDir}
-        '';
-      serviceConfig.ExecStart = "${pkgs.trousers}/sbin/tcsd -f -c ${tcsdConf}";
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.trousers}/sbin/tcsd -f -c ${tcsdConf}";
+      };
     };
 
     users.users = optionalAttrs (cfg.user == "tss") {
diff --git a/nixos/modules/services/logging/graylog.nix b/nixos/modules/services/logging/graylog.nix
index a889a44d4b2..af70d27fcf9 100644
--- a/nixos/modules/services/logging/graylog.nix
+++ b/nixos/modules/services/logging/graylog.nix
@@ -39,7 +39,6 @@ in
         type = types.package;
         default = pkgs.graylog;
         defaultText = "pkgs.graylog";
-        example = literalExample "pkgs.graylog";
         description = "Graylog package to use.";
       };
 
@@ -138,14 +137,13 @@ in
       "d '${cfg.messageJournalDir}' - ${cfg.user} - - -"
     ];
 
-    systemd.services.graylog = with pkgs; {
+    systemd.services.graylog = {
       description = "Graylog Server";
       wantedBy = [ "multi-user.target" ];
       environment = {
-        JAVA_HOME = jre;
         GRAYLOG_CONF = "${confFile}";
       };
-      path = [ pkgs.jre_headless pkgs.which pkgs.procps ];
+      path = [ pkgs.which pkgs.procps ];
       preStart = ''
         rm -rf /var/lib/graylog/plugins || true
         mkdir -p /var/lib/graylog/plugins -m 755
diff --git a/nixos/modules/services/logging/logstash.nix b/nixos/modules/services/logging/logstash.nix
index a4fc315d080..7a2f5681612 100644
--- a/nixos/modules/services/logging/logstash.nix
+++ b/nixos/modules/services/logging/logstash.nix
@@ -159,10 +159,9 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
-    systemd.services.logstash = with pkgs; {
+    systemd.services.logstash = {
       description = "Logstash Daemon";
       wantedBy = [ "multi-user.target" ];
-      environment = { JAVA_HOME = jre; };
       path = [ pkgs.bash ];
       serviceConfig = {
         ExecStartPre = ''${pkgs.coreutils}/bin/mkdir -p "${cfg.dataDir}" ; ${pkgs.coreutils}/bin/chmod 700 "${cfg.dataDir}"'';
diff --git a/nixos/modules/services/logging/promtail.nix b/nixos/modules/services/logging/promtail.nix
index 19b12daa415..34211687dc1 100644
--- a/nixos/modules/services/logging/promtail.nix
+++ b/nixos/modules/services/logging/promtail.nix
@@ -40,6 +40,7 @@ in {
 
       serviceConfig = {
         Restart = "on-failure";
+        TimeoutStopSec = 10;
 
         ExecStart = "${pkgs.grafana-loki}/bin/promtail -config.file=${prettyJSON cfg.configuration} ${escapeShellArgs cfg.extraFlags}";
 
diff --git a/nixos/modules/services/logging/vector.nix b/nixos/modules/services/logging/vector.nix
index a7c54ad75fd..be36b2a41bb 100644
--- a/nixos/modules/services/logging/vector.nix
+++ b/nixos/modules/services/logging/vector.nix
@@ -3,7 +3,8 @@
 with lib;
 let cfg = config.services.vector;
 
-in {
+in
+{
   options.services.vector = {
     enable = mkEnableOption "Vector";
 
@@ -37,25 +38,27 @@ in {
       wantedBy = [ "multi-user.target" ];
       after = [ "network-online.target" ];
       requires = [ "network-online.target" ];
-      serviceConfig = let
-        format = pkgs.formats.toml { };
-        conf = format.generate "vector.toml" cfg.settings;
-        validateConfig = file:
-          pkgs.runCommand "validate-vector-conf" { } ''
-            ${pkgs.vector}/bin/vector validate --no-topology --no-environment "${file}"
-            ln -s "${file}" "$out"
-          '';
-      in {
-        ExecStart = "${pkgs.vector}/bin/vector --config ${validateConfig conf}";
-        User = "vector";
-        Group = "vector";
-        Restart = "no";
-        StateDirectory = "vector";
-        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
-        # This group is required for accessing journald.
-        SupplementaryGroups = mkIf cfg.journaldAccess "systemd-journal";
-      };
+      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/dovecot.nix b/nixos/modules/services/mail/dovecot.nix
index a2298152b02..1ccfb357750 100644
--- a/nixos/modules/services/mail/dovecot.nix
+++ b/nixos/modules/services/mail/dovecot.nix
@@ -405,7 +405,7 @@ in
         };
     } // optionalAttrs (cfg.createMailUser && cfg.mailUser != null) {
       ${cfg.mailUser} =
-        { description = "Virtual Mail User"; } // optionalAttrs (cfg.mailGroup != null)
+        { description = "Virtual Mail User"; isSystemUser = true; } // optionalAttrs (cfg.mailGroup != null)
           { group = cfg.mailGroup; };
     };
 
diff --git a/nixos/modules/services/mail/exim.nix b/nixos/modules/services/mail/exim.nix
index 892fbd33214..8927d84b478 100644
--- a/nixos/modules/services/mail/exim.nix
+++ b/nixos/modules/services/mail/exim.nix
@@ -67,6 +67,13 @@ in
         '';
       };
 
+      queueRunnerInterval = mkOption {
+        type = types.str;
+        default = "5m";
+        description = ''
+          How often to spawn a new queue runner.
+        '';
+      };
     };
 
   };
@@ -104,7 +111,7 @@ in
       wantedBy = [ "multi-user.target" ];
       restartTriggers = [ config.environment.etc."exim.conf".source ];
       serviceConfig = {
-        ExecStart   = "${cfg.package}/bin/exim -bdf -q30m";
+        ExecStart   = "${cfg.package}/bin/exim -bdf -q${cfg.queueRunnerInterval}";
         ExecReload  = "${coreutils}/bin/kill -HUP $MAINPID";
       };
       preStart = ''
diff --git a/nixos/modules/services/mail/mailman.nix b/nixos/modules/services/mail/mailman.nix
index 832b496f31c..f75d6183c3e 100644
--- a/nixos/modules/services/mail/mailman.nix
+++ b/nixos/modules/services/mail/mailman.nix
@@ -165,7 +165,7 @@ in {
 
         baseUrl = mkOption {
           type = types.str;
-          default = "http://localhost/hyperkitty/";
+          default = "http://localhost:18507/archives/";
           description = ''
             Where can Mailman connect to Hyperkitty's internal API, preferably on
             localhost?
@@ -263,7 +263,8 @@ in {
       # settings_local.json is loaded.
       os.environ["SECRET_KEY"] = ""
 
-      from mailman_web.settings import *
+      from mailman_web.settings.base import *
+      from mailman_web.settings.mailman import *
 
       import json
 
@@ -390,6 +391,7 @@ in {
           plugins = ["python3"];
           home = pythonEnv;
           module = "mailman_web.wsgi";
+          http = "127.0.0.1:18507";
         };
         uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
       in {
@@ -452,7 +454,7 @@ in {
   };
 
   meta = {
-    maintainers = with lib.maintainers; [ lheckemann ];
+    maintainers = with lib.maintainers; [ lheckemann qyliss ];
     doc = ./mailman.xml;
   };
 
diff --git a/nixos/modules/services/mail/nullmailer.nix b/nixos/modules/services/mail/nullmailer.nix
index fe3f8ef9b39..09874ca0ed7 100644
--- a/nixos/modules/services/mail/nullmailer.nix
+++ b/nixos/modules/services/mail/nullmailer.nix
@@ -204,6 +204,7 @@ with lib;
       users.${cfg.user} = {
         description = "Nullmailer relay-only mta user";
         group = cfg.group;
+        isSystemUser = true;
       };
 
       groups.${cfg.group} = { };
diff --git a/nixos/modules/services/mail/opendkim.nix b/nixos/modules/services/mail/opendkim.nix
index 9bf6f338d93..beff57613af 100644
--- a/nixos/modules/services/mail/opendkim.nix
+++ b/nixos/modules/services/mail/opendkim.nix
@@ -134,7 +134,7 @@ in {
         ReadWritePaths = [ cfg.keyPath ];
 
         AmbientCapabilities = [];
-        CapabilityBoundingSet = [];
+        CapabilityBoundingSet = "";
         DevicePolicy = "closed";
         LockPersonality = true;
         MemoryDenyWriteExecute = true;
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index 63c0961b756..35639e1bbc8 100644
--- a/nixos/modules/services/mail/postfix.nix
+++ b/nixos/modules/services/mail/postfix.nix
@@ -11,6 +11,7 @@ let
 
   haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != ""
                       || cfg.extraAliases != "";
+  haveCanonical = cfg.canonical != "";
   haveTransport = cfg.transport != "";
   haveVirtual = cfg.virtual != "";
   haveLocalRecipients = cfg.localRecipients != null;
@@ -244,6 +245,7 @@ let
   ;
 
   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;
@@ -529,6 +531,15 @@ in
         ";
       };
 
+      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 = "";
@@ -762,7 +773,7 @@ in
         };
 
       services.postfix.config = (mapAttrs (_: v: mkDefault v) {
-        compatibility_level  = "9999";
+        compatibility_level  = pkgs.postfix.version;
         mail_owner           = cfg.user;
         default_privs        = "nobody";
 
@@ -941,6 +952,9 @@ in
     (mkIf haveAliases {
       services.postfix.aliasFiles.aliases = aliasesFile;
     })
+    (mkIf haveCanonical {
+      services.postfix.mapFiles.canonical = canonicalFile;
+    })
     (mkIf haveTransport {
       services.postfix.mapFiles.transport = transportFile;
     })
diff --git a/nixos/modules/services/mail/rspamd.nix b/nixos/modules/services/mail/rspamd.nix
index 2f9d28195bd..473ddd52357 100644
--- a/nixos/modules/services/mail/rspamd.nix
+++ b/nixos/modules/services/mail/rspamd.nix
@@ -410,7 +410,7 @@ in
         StateDirectoryMode = "0700";
 
         AmbientCapabilities = [];
-        CapabilityBoundingSet = [];
+        CapabilityBoundingSet = "";
         DevicePolicy = "closed";
         LockPersonality = true;
         NoNewPrivileges = true;
diff --git a/nixos/modules/services/mail/spamassassin.nix b/nixos/modules/services/mail/spamassassin.nix
index 4e642542ec6..ac878222b26 100644
--- a/nixos/modules/services/mail/spamassassin.nix
+++ b/nixos/modules/services/mail/spamassassin.nix
@@ -126,19 +126,36 @@ in
     };
 
     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.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/" spamd
-
-        v=$?
+        ${pkgs.spamassassin}/bin/sa-update --verbose --gpghomedir=/var/lib/spamassassin/sa-update-keys/
+        rc=$?
         set -e
-        if [ $v -gt 1 ]; then
-          echo "sa-update execution error"
-          exit $v
+
+        if [[ $rc -gt 1 ]]; then
+          # sa-update failed.
+          exit $rc
         fi
-        if [ $v -eq 0 ]; then
-          systemctl reload spamd.service
+
+        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
       '';
     };
 
@@ -153,32 +170,22 @@ in
     };
 
     systemd.services.spamd = {
-      description = "Spam Assassin Server";
+      description = "SpamAssassin Server";
 
       wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
+      wants = [ "sa-update.service" ];
+      after = [
+        "network.target"
+        "sa-update.service"
+      ];
 
       serviceConfig = {
-        ExecStart = "${pkgs.spamassassin}/bin/spamd ${optionalString cfg.debug "-D"} --username=spamd --groupname=spamd --virtual-config-dir=/var/lib/spamassassin/user-%u --allow-tell --pidfile=/run/spamd.pid";
-        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        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";
       };
-
-      # 0 and 1 no error, exitcode > 1 means error:
-      # https://spamassassin.apache.org/full/3.1.x/doc/sa-update.html#exit_codes
-      preStart = ''
-        echo "Recreating '/var/lib/spamasassin' with creating '3.004001' (or similar) and 'sa-update-keys'"
-        mkdir -p /var/lib/spamassassin
-        chown spamd:spamd /var/lib/spamassassin -R
-        set +e
-        ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/" spamd
-        v=$?
-        set -e
-        if [ $v -gt 1 ]; then
-          echo "sa-update execution error"
-          exit $v
-        fi
-        chown spamd:spamd /var/lib/spamassassin -R
-      '';
     };
   };
 }
diff --git a/nixos/modules/services/misc/airsonic.nix b/nixos/modules/services/misc/airsonic.nix
index 5cc2ff7f4bd..a572f1f6d6f 100644
--- a/nixos/modules/services/misc/airsonic.nix
+++ b/nixos/modules/services/misc/airsonic.nix
@@ -118,7 +118,7 @@ in {
       '';
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.jre}/bin/java -Xmx${toString cfg.maxMemory}m \
+          ${pkgs.jre8}/bin/java -Xmx${toString cfg.maxMemory}m \
           -Dairsonic.home=${cfg.home} \
           -Dserver.address=${cfg.listenAddress} \
           -Dserver.port=${toString cfg.port} \
diff --git a/nixos/modules/services/misc/apache-kafka.nix b/nixos/modules/services/misc/apache-kafka.nix
index f3a650a260f..69dfadfe54e 100644
--- a/nixos/modules/services/misc/apache-kafka.nix
+++ b/nixos/modules/services/misc/apache-kafka.nix
@@ -90,19 +90,7 @@ in {
 
     jvmOptions = mkOption {
       description = "Extra command line options for the JVM running Kafka.";
-      default = [
-        "-server"
-        "-Xmx1G"
-        "-Xms1G"
-        "-XX:+UseCompressedOops"
-        "-XX:+UseParNewGC"
-        "-XX:+UseConcMarkSweepGC"
-        "-XX:+CMSClassUnloadingEnabled"
-        "-XX:+CMSScavengeBeforeRemark"
-        "-XX:+DisableExplicitGC"
-        "-Djava.awt.headless=true"
-        "-Djava.net.preferIPv4Stack=true"
-      ];
+      default = [];
       type = types.listOf types.str;
       example = [
         "-Djava.net.preferIPv4Stack=true"
@@ -118,6 +106,13 @@ in {
       type = types.package;
     };
 
+    jre = mkOption {
+      description = "The JRE with which to run Kafka";
+      default = cfg.package.passthru.jre;
+      defaultText = "pkgs.apacheKafka.passthru.jre";
+      type = types.package;
+    };
+
   };
 
   config = mkIf cfg.enable {
@@ -138,7 +133,7 @@ in {
       after = [ "network.target" ];
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.jre}/bin/java \
+          ${cfg.jre}/bin/java \
             -cp "${cfg.package}/libs/*" \
             -Dlog4j.configuration=file:${logConfig} \
             ${toString cfg.jvmOptions} \
diff --git a/nixos/modules/services/misc/bazarr.nix b/nixos/modules/services/misc/bazarr.nix
index d3fd5b08cc8..99343a146a7 100644
--- a/nixos/modules/services/misc/bazarr.nix
+++ b/nixos/modules/services/misc/bazarr.nix
@@ -64,6 +64,7 @@ in
 
     users.users = mkIf (cfg.user == "bazarr") {
       bazarr = {
+        isSystemUser = true;
         group = cfg.group;
         home = "/var/lib/${config.systemd.services.bazarr.serviceConfig.StateDirectory}";
       };
diff --git a/nixos/modules/services/misc/defaultUnicornConfig.rb b/nixos/modules/services/misc/defaultUnicornConfig.rb
deleted file mode 100644
index 0b58c59c7a5..00000000000
--- a/nixos/modules/services/misc/defaultUnicornConfig.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-worker_processes 3
-
-listen ENV["UNICORN_PATH"] + "/tmp/sockets/gitlab.socket", :backlog => 1024
-listen "/run/gitlab/gitlab.socket", :backlog => 1024
-
-working_directory ENV["GITLAB_PATH"]
-
-pid ENV["UNICORN_PATH"] + "/tmp/pids/unicorn.pid"
-
-timeout 60
-
-# combine Ruby 2.0.0dev or REE with "preload_app true" for memory savings
-# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
-preload_app true
-GC.respond_to?(:copy_on_write_friendly=) and
-  GC.copy_on_write_friendly = true
-
-check_client_connection false
-
-before_fork do |server, worker|
-  # the following is highly recommended for Rails + "preload_app true"
-  # as there's no need for the master process to hold a connection
-  defined?(ActiveRecord::Base) and
-    ActiveRecord::Base.connection.disconnect!
-
-  # The following is only recommended for memory/DB-constrained
-  # installations.  It is not needed if your system can house
-  # twice as many worker_processes as you have configured.
-  #
-  # This allows a new master process to incrementally
-  # phase out the old master process with SIGTTOU to avoid a
-  # thundering herd (especially in the "preload_app false" case)
-  # when doing a transparent upgrade.  The last worker spawned
-  # will then kill off the old master process with a SIGQUIT.
-  old_pid = "#{server.config[:pid]}.oldbin"
-  if old_pid != server.pid
-    begin
-      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
-      Process.kill(sig, File.read(old_pid).to_i)
-    rescue Errno::ENOENT, Errno::ESRCH
-    end
-  end
-
-  # Throttle the master from forking too quickly by sleeping.  Due
-  # to the implementation of standard Unix signal handlers, this
-  # helps (but does not completely) prevent identical, repeated signals
-  # from being lost when the receiving process is busy.
-  # sleep 1
-end
-
-after_fork do |server, worker|
-  # per-process listener ports for debugging/admin/migrations
-  # addr = "127.0.0.1:#{9293 + worker.nr}"
-  # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)
-
-  # the following is *required* for Rails + "preload_app true",
-  defined?(ActiveRecord::Base) and
-    ActiveRecord::Base.establish_connection
-
-  # reset prometheus client, this will cause any opened metrics files to be closed
-  defined?(::Prometheus::Client.reinitialize_on_pid_change) &&
-    Prometheus::Client.reinitialize_on_pid_change
-
-  # if preload_app is true, then you may also want to check and
-  # restart any other shared sockets/descriptors such as Memcached,
-  # and Redis.  TokyoCabinet file handles are safe to reuse
-  # between any number of forked children (assuming your kernel
-  # correctly implements pread()/pwrite() system calls)
-end
diff --git a/nixos/modules/services/misc/disnix.nix b/nixos/modules/services/misc/disnix.nix
new file mode 100644
index 00000000000..24a259bb4d2
--- /dev/null
+++ b/nixos/modules/services/misc/disnix.nix
@@ -0,0 +1,99 @@
+# Disnix server
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.disnix;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.disnix = {
+
+      enable = mkEnableOption "Disnix";
+
+      enableMultiUser = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to support multi-user mode by enabling the Disnix D-Bus service";
+      };
+
+      useWebServiceInterface = mkEnableOption "the DisnixWebService interface running on Apache Tomcat";
+
+      package = mkOption {
+        type = types.path;
+        description = "The Disnix package";
+        default = pkgs.disnix;
+        defaultText = "pkgs.disnix";
+      };
+
+      enableProfilePath = mkEnableOption "exposing the Disnix profiles in the system's PATH";
+
+      profiles = mkOption {
+        type = types.listOf types.str;
+        default = [ "default" ];
+        example = [ "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/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/dysnomia.nix b/nixos/modules/services/misc/dysnomia.nix
new file mode 100644
index 00000000000..333ba651cde
--- /dev/null
+++ b/nixos/modules/services/misc/dysnomia.nix
@@ -0,0 +1,259 @@
+{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 = {};
+      };
+
+      containers = mkOption {
+        description = "An attribute set in which each key represents a container and each value an attribute set providing its configuration properties";
+        default = {};
+      };
+
+      components = mkOption {
+        description = "An atttribute set in which each key represents a container and each value an attribute set in which each key represents a component and each value a derivation constructing its initial state";
+        default = {};
+      };
+
+      extraContainerProperties = mkOption {
+        description = "An attribute set providing additional container settings in addition to the default properties";
+        default = {};
+      };
+
+      extraContainerPaths = mkOption {
+        description = "A list of paths containing additional container configurations that are added to the search folders";
+        default = [];
+      };
+
+      extraModulePaths = mkOption {
+        description = "A list of paths containing additional modules that are added to the search folders";
+        default = [];
+      };
+
+      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/etebase-server.nix b/nixos/modules/services/misc/etebase-server.nix
index d9d12698d79..b6bd6e9fd37 100644
--- a/nixos/modules/services/misc/etebase-server.nix
+++ b/nixos/modules/services/misc/etebase-server.nix
@@ -8,31 +8,28 @@ let
   pythonEnv = pkgs.python3.withPackages (ps: with ps;
     [ etebase-server daphne ]);
 
-  dbConfig = {
-    sqlite3 = ''
-      engine = django.db.backends.sqlite3
-      name = ${cfg.dataDir}/db.sqlite3
-    '';
-  };
-
-  defaultConfigIni = toString (pkgs.writeText "etebase-server.ini" ''
-    [global]
-    debug = false
-    secret_file = ${if cfg.secretFile != null then cfg.secretFile else ""}
-    media_root = ${cfg.dataDir}/media
-
-    [allowed_hosts]
-    allowed_host1 = ${cfg.host}
+  iniFmt = pkgs.formats.ini {};
 
-    [database]
-    ${dbConfig."${cfg.database.type}"}
-  '');
-
-  configIni = if cfg.customIni != null then cfg.customIni else defaultConfigIni;
+  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 {
@@ -42,21 +39,13 @@ in
         description = ''
           Whether to enable the Etebase server.
 
-          Once enabled you need to create an admin user using the
-          shell command <literal>etebase-server createsuperuser</literal>.
+          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
         '';
       };
 
-      secretFile = mkOption {
-        default = null;
-        type = with types; nullOr str;
-        description = ''
-          The path to a file containing the secret
-          used as django's SECRET_KEY.
-        '';
-      };
-
       dataDir = mkOption {
         type = types.str;
         default = "/var/lib/etebase-server";
@@ -77,15 +66,6 @@ in
         '';
       };
 
-      host = mkOption {
-        type = types.str;
-        default = "0.0.0.0";
-        example = "localhost";
-        description = ''
-          Host to listen on.
-        '';
-      };
-
       unixSocket = mkOption {
         type = with types; nullOr str;
         default = null;
@@ -93,42 +73,81 @@ in
         example = "/run/etebase-server/etebase-server.sock";
       };
 
-      database = {
-        type = mkOption {
-          type = types.enum [ "sqlite3" ];
-          default = "sqlite3";
-          description = ''
-            Database engine to use.
-            Currently only sqlite3 is supported.
-            Other options can be configured using <literal>extraConfig</literal>.
-          '';
+      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 = "\${config.services.etebase-server.dataDir}/static";
+                description = "The directory for static files.";
+              };
+              media_root = mkOption {
+                type = types.str;
+                default = "${cfg.dataDir}/media";
+                defaultText = "\${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 = "\${config.services.etebase-server.dataDir}/db.sqlite3";
+                description = "The database name.";
+              };
+            };
+          };
         };
-      };
-
-      customIni = mkOption {
-        type = with types; nullOr str;
-        default = null;
+        default = {};
         description = ''
-          Custom etebase-server.ini.
-
-          See <literal>etebase-src/etebase-server.ini.example</literal> for available options.
-
-          Setting this option overrides the default config which is generated from the options
-          <literal>secretFile</literal>, <literal>host</literal> and <literal>database</literal>.
-        '';
-        example = literalExample ''
-          [global]
-          debug = false
-          secret_file = /path/to/secret
-          media_root = /path/to/media
-
-          [allowed_hosts]
-          allowed_host1 = example.com
-
-          [database]
-          engine = django.db.backends.sqlite3
-          name = db.sqlite3
+          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 {
@@ -166,14 +185,15 @@ in
         WorkingDirectory = cfg.dataDir;
       };
       environment = {
-        PYTHONPATH="${pythonEnv}/${pkgs.python3.sitePackages}";
-        ETEBASE_EASY_CONFIG_PATH="${configIni}";
+        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
+          ${pythonEnv}/bin/etebase-server migrate --no-input
+          ${pythonEnv}/bin/etebase-server collectstatic --no-input --clear
           echo ${pkgs.etebase-server} > "$versionFile"
         fi
       '';
@@ -191,6 +211,7 @@ in
 
     users = optionalAttrs (cfg.user == defaultUser) {
       users.${defaultUser} = {
+        isSystemUser = true;
         group = defaultUser;
         home = cfg.dataDir;
       };
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index 434e2d2429b..95369ff7ee4 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -477,47 +477,49 @@ in
       in ''
         # copy custom configuration and generate a random secret key if needed
         ${optionalString (cfg.useWizard == false) ''
-          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}
-          chmod 640 ${runConfig} ${secretKey} ${oauth2JwtSecret} ${lfsJwtSecret} ${internalToken}
+          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)
         ''}
 
         # update all hooks' binary paths
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index 61faeab7d32..8153754af0f 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -116,7 +116,12 @@ let
       omniauth.enabled = false;
       shared.path = "${cfg.statePath}/shared";
       gitaly.client_path = "${cfg.packages.gitaly}/bin";
-      backup.path = "${cfg.backupPath}";
+      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";
@@ -142,7 +147,7 @@ let
 
   gitlabEnv = {
     HOME = "${cfg.statePath}/home";
-    UNICORN_PATH = "${cfg.statePath}/";
+    PUMA_PATH = "${cfg.statePath}/";
     GITLAB_PATH = "${cfg.packages.gitlab}/share/gitlab/";
     SCHEMA = "${cfg.statePath}/db/structure.sql";
     GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads";
@@ -150,6 +155,7 @@ let
     GITLAB_REDIS_CONFIG_FILE = pkgs.writeText "redis.yml" (builtins.toJSON redisConfig);
     prometheus_multiproc_dir = "/run/gitlab";
     RAILS_ENV = "production";
+    MALLOC_ARENA_MAX = "2";
   };
 
   gitlab-rake = pkgs.stdenv.mkDerivation {
@@ -196,6 +202,7 @@ let
         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}'
       }
@@ -206,6 +213,7 @@ in {
 
   imports = [
     (mkRenamedOptionModule [ "services" "gitlab" "stateDir" ] [ "services" "gitlab" "statePath" ])
+    (mkRenamedOptionModule [ "services" "gitlab" "backupPath" ] [ "services" "gitlab" "backup" "path" ])
     (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "")
   ];
 
@@ -259,7 +267,7 @@ in {
         type = types.str;
         default = "/var/gitlab/state";
         description = ''
-          Gitlab state directory. Configuration, repositories and
+          GitLab state directory. Configuration, repositories and
           logs, among other things, are stored here.
 
           The directory will be created automatically if it doesn't
@@ -269,17 +277,108 @@ in {
         '';
       };
 
-      backupPath = mkOption {
+      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";
-        description = "Gitlab path for backups.";
+        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 = literalExample ''
+          {
+            # 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
+          GitLab database hostname. An empty string means <quote>use
           local unix socket connection</quote>.
         '';
       };
@@ -288,7 +387,7 @@ in {
         type = with types; nullOr path;
         default = null;
         description = ''
-          File containing the Gitlab database user password.
+          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.
@@ -309,13 +408,13 @@ in {
       databaseName = mkOption {
         type = types.str;
         default = "gitlab";
-        description = "Gitlab database name.";
+        description = "GitLab database name.";
       };
 
       databaseUsername = mkOption {
         type = types.str;
         default = "gitlab";
-        description = "Gitlab database user.";
+        description = "GitLab database user.";
       };
 
       databasePool = mkOption {
@@ -359,14 +458,14 @@ in {
       host = mkOption {
         type = types.str;
         default = config.networking.hostName;
-        description = "Gitlab host name. Used e.g. for copy-paste URLs.";
+        description = "GitLab host name. Used e.g. for copy-paste URLs.";
       };
 
       port = mkOption {
         type = types.int;
         default = 8080;
         description = ''
-          Gitlab server port for copy-paste URLs, e.g. 80 or 443 if you're
+          GitLab server port for copy-paste URLs, e.g. 80 or 443 if you're
           service over https.
         '';
       };
@@ -419,26 +518,26 @@ in {
         address = mkOption {
           type = types.str;
           default = "localhost";
-          description = "Address of the SMTP server for Gitlab.";
+          description = "Address of the SMTP server for GitLab.";
         };
 
         port = mkOption {
           type = types.int;
-          default = 465;
-          description = "Port of the SMTP server for Gitlab.";
+          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.";
+          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.
+            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.
@@ -463,6 +562,12 @@ in {
           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";
@@ -484,7 +589,7 @@ 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 30 characters and all random,
+          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
@@ -500,7 +605,7 @@ 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 30 characters and all random,
+          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
@@ -516,7 +621,7 @@ in {
           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 30 characters and all random,
+          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
@@ -548,6 +653,105 @@ in {
         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.
+        '';
+      };
+
       extraConfig = mkOption {
         type = types.attrs;
         default = {};
@@ -641,6 +845,11 @@ in {
 
     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.enable = mkDefault true;
 
@@ -655,36 +864,45 @@ in {
     # here.
     systemd.services.gitlab-postgresql = let pgsql = config.services.postgresql; in mkIf databaseActuallyCreateLocally {
       after = [ "postgresql.service" ];
-      wantedBy = [ "multi-user.target" ];
-      path = [ pgsql.package ];
+      bindsTo = [ "postgresql.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      path = [
+        pgsql.package
+        pkgs.util-linux
+      ];
       script = ''
         set -eu
 
-        PSQL="${pkgs.util-linux}/bin/runuser -u ${pgsql.superUser} -- psql --port=${toString pgsql.port}"
+        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}'")
+        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}"'
+            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}\""
+            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;"
+        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;
       };
     };
 
     # Use postfix to send out mails.
-    services.postfix.enable = mkDefault true;
+    services.postfix.enable = mkDefault (cfg.smtp.enable && cfg.smtp.address == "localhost");
 
     users.users.${cfg.user} =
       { group = cfg.group;
@@ -699,11 +917,10 @@ in {
       "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.backupPath} 0750 ${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}/config/initializers 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} -"
@@ -726,14 +943,161 @@ in {
       "L+ /run/gitlab/uploads - - - - ${cfg.statePath}/uploads"
 
       "L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}"
-
-      "L+ ${cfg.statePath}/config/unicorn.rb - - - - ${./defaultUnicornConfig.rb}"
     ];
 
+
+    systemd.services.gitlab-config = {
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      path = with pkgs; [
+        jq
+        openssl
+        replace
+        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 = ''
+            shopt -s dotglob nullglob
+            set -eu
+
+            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 -eu
+
+          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) ''
+                  smtp_password=$(<'${cfg.smtp.passwordFile}')
+                  replace-literal -e '@smtpPassword@' "$smtp_password" '${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 ''
+                export db_password="$(<'${cfg.databasePasswordFile}')"
+
+                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'
+
+            export secret="$(<'${cfg.secrets.secretFile}')"
+            export db="$(<'${cfg.secrets.dbFile}')"
+            export otp="$(<'${cfg.secrets.otpFile}')"
+            export jws="$(<'${cfg.secrets.jwsFile}')"
+            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 -eu
+          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.service" "gitlab.service" ];
-      wantedBy = [ "multi-user.target" ];
-      environment = gitlabEnv;
+      after = [
+        "network.target"
+        "redis.service"
+        "postgresql.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ];
+      bindsTo = [
+        "redis.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
@@ -745,22 +1109,25 @@ in {
         # Needed for GitLab project imports
         gnutar
         gzip
+
+        procps # Sidekiq MemoryKiller
       ];
       serviceConfig = {
         Type = "simple";
         User = cfg.user;
         Group = cfg.group;
         TimeoutSec = "infinity";
-        Restart = "on-failure";
+        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.service" ];
-      bindsTo = [ "gitlab.service" ];
-      wantedBy = [ "multi-user.target" ];
+      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
@@ -783,8 +1150,10 @@ in {
 
     systemd.services.gitlab-pages = mkIf (gitlabConfig.production.pages.enabled or false) {
       description = "GitLab static pages daemon";
-      after = [ "network.target" "redis.service" "gitlab.service" ]; # gitlab.service creates configs
-      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "gitlab-config.service" ];
+      bindsTo = [ "gitlab-config.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
 
       path = [ pkgs.unzip ];
 
@@ -803,7 +1172,8 @@ in {
 
     systemd.services.gitlab-workhorse = {
       after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
       path = with pkgs; [
         exiftool
         git
@@ -832,8 +1202,10 @@ in {
 
     systemd.services.gitlab-mailroom = mkIf (gitlabConfig.production.incoming_email.enabled or false) {
       description = "GitLab incoming mail daemon";
-      after = [ "network.target" "redis.service" "gitlab.service" ]; # gitlab.service creates configs
-      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "redis.service" "gitlab-config.service" ];
+      bindsTo = [ "gitlab-config.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
       environment = gitlabEnv;
       serviceConfig = {
         Type = "simple";
@@ -842,15 +1214,26 @@ in {
 
         User = cfg.user;
         Group = cfg.group;
-        ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/bundle exec mail_room -c ${cfg.packages.gitlab}/share/gitlab/config.dist/mail_room.yml";
+        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" "gitlab-postgresql.service" "redis.service" ];
-      requires = [ "gitlab-sidekiq.service" ];
-      wantedBy = [ "multi-user.target" ];
+      after = [
+        "gitlab-workhorse.service"
+        "network.target"
+        "redis.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ];
+      bindsTo = [
+        "redis.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
@@ -868,100 +1251,34 @@ in {
         TimeoutSec = "infinity";
         Restart = "on-failure";
         WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
-        ExecStartPre = let
-          preStartFullPrivileges = ''
-            shopt -s dotglob nullglob
-            set -eu
-
-            chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/*
-            chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/config/*
-          '';
-          preStart = ''
-            set -eu
-
-            cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
-            rm -rf ${cfg.statePath}/db/*
-            rm -rf ${cfg.statePath}/config/initializers/*
-            rm -f ${cfg.statePath}/lib
-            cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
-            cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db
-            ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb
-
-            ${cfg.packages.gitlab-shell}/bin/install
-
-            ${optionalString cfg.smtp.enable ''
-              install -m u=rw ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb
-              ${optionalString (cfg.smtp.passwordFile != null) ''
-                smtp_password=$(<'${cfg.smtp.passwordFile}')
-                ${pkgs.replace}/bin/replace-literal -e '@smtpPassword@' "$smtp_password" '${cfg.statePath}/config/initializers/smtp_settings.rb'
-              ''}
-            ''}
-
-            (
-              umask u=rwx,g=,o=
-
-              ${pkgs.openssl}/bin/openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
-
-              if [[ -h '${cfg.statePath}/config/database.yml' ]]; then
-                rm '${cfg.statePath}/config/database.yml'
-              fi
-
-              ${if cfg.databasePasswordFile != null then ''
-                  export db_password="$(<'${cfg.databasePasswordFile}')"
-
-                  if [[ -z "$db_password" ]]; then
-                    >&2 echo "Database password was an empty string!"
-                    exit 1
-                  fi
-
-                  ${pkgs.jq}/bin/jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
-                                    '.production.password = $ENV.db_password' \
-                                    >'${cfg.statePath}/config/database.yml'
-                ''
-                else ''
-                  ${pkgs.jq}/bin/jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
-                                    >'${cfg.statePath}/config/database.yml'
-                ''
-              }
-
-              ${utils.genJqSecretsReplacementSnippet
-                  gitlabConfig
-                  "${cfg.statePath}/config/gitlab.yml"
-              }
-
-              if [[ -h '${cfg.statePath}/config/secrets.yml' ]]; then
-                rm '${cfg.statePath}/config/secrets.yml'
-              fi
-
-              export secret="$(<'${cfg.secrets.secretFile}')"
-              export db="$(<'${cfg.secrets.dbFile}')"
-              export otp="$(<'${cfg.secrets.otpFile}')"
-              export jws="$(<'${cfg.secrets.jwsFile}')"
-              ${pkgs.jq}/bin/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'
-            )
-
-            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
-
-            # We remove potentially broken links to old gitlab-shell versions
-            rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks
-
-            ${pkgs.git}/bin/git config --global core.autocrlf "input"
-          '';
-        in [
-          "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}"
-          "${pkgs.writeShellScript "gitlab-pre-start" preStart}"
+        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}"
         ];
-        ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/unicorn -c ${cfg.statePath}/config/unicorn.rb -E production";
       };
 
     };
 
+    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
index 19a3df0a5f6..40424c5039a 100644
--- a/nixos/modules/services/misc/gitlab.xml
+++ b/nixos/modules/services/misc/gitlab.xml
@@ -3,15 +3,15 @@
          xmlns:xi="http://www.w3.org/2001/XInclude"
          version="5.0"
          xml:id="module-services-gitlab">
- <title>Gitlab</title>
+ <title>GitLab</title>
  <para>
-  Gitlab is a feature-rich git hosting service.
+  GitLab is a feature-rich git hosting service.
  </para>
  <section xml:id="module-services-gitlab-prerequisites">
   <title>Prerequisites</title>
 
   <para>
-   The gitlab service exposes only an Unix socket at
+   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>
@@ -39,7 +39,7 @@
   <title>Configuring</title>
 
   <para>
-   Gitlab depends on both PostgreSQL and Redis and will automatically enable
+   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>
@@ -85,20 +85,20 @@ services.gitlab = {
   </para>
 
   <para>
-   If you're setting up a new Gitlab instance, generate new
+   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
+   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
+   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>icoming_mail.enabled</literal> is set to <literal>true</literal>
+    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>
@@ -112,21 +112,40 @@ services.gitlab = {
  <section xml:id="module-services-gitlab-maintenance">
   <title>Maintenance</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>
+  <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>
-   For example, to backup a Gitlab instance:
+   <para>
+     To run a manual backup, start the <literal>gitlab-backup</literal> service:
 <screen>
-<prompt>$ </prompt>sudo -u git -H gitlab-rake gitlab:backup:create
+<prompt>$ </prompt>systemctl start gitlab-backup.service
 </screen>
-   A list of all availabe rake tasks can be obtained by running:
+   </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>
+   </para>
+  </section>
  </section>
 </chapter>
diff --git a/nixos/modules/services/misc/gollum.nix b/nixos/modules/services/misc/gollum.nix
index 0c9c7548305..4053afa69be 100644
--- a/nixos/modules/services/misc/gollum.nix
+++ b/nixos/modules/services/misc/gollum.nix
@@ -115,4 +115,6 @@ in
       };
     };
   };
+
+  meta.maintainers = with lib.maintainers; [ erictapen ];
 }
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix
index 1f2e13f3732..1e33381de24 100644
--- a/nixos/modules/services/misc/home-assistant.nix
+++ b/nixos/modules/services/misc/home-assistant.nix
@@ -63,7 +63,7 @@ let
   };
 
 in {
-  meta.maintainers = with maintainers; [ dotlambda ];
+  meta.maintainers = teams.home-assistant.members;
 
   options.services.home-assistant = {
     enable = mkEnableOption "Home Assistant";
@@ -183,8 +183,14 @@ in {
     };
 
     package = mkOption {
-      default = pkgs.home-assistant;
-      defaultText = "pkgs.home-assistant";
+      default = pkgs.home-assistant.overrideAttrs (oldAttrs: {
+        doInstallCheck = false;
+      });
+      defaultText = literalExample ''
+        pkgs.home-assistant.overrideAttrs (oldAttrs: {
+          doInstallCheck = false;
+        })
+      '';
       type = types.package;
       example = literalExample ''
         pkgs.home-assistant.override {
@@ -192,10 +198,11 @@ in {
         }
       '';
       description = ''
-        Home Assistant package to use.
+        Home Assistant package to use. By default the tests are disabled, as they take a considerable amout of time to complete.
         Override <literal>extraPackages</literal> or <literal>extraComponents</literal> in order to add additional dependencies.
         If you specify <option>config</option> and do not set <option>autoExtraComponents</option>
         to <literal>false</literal>, overriding <literal>extraComponents</literal> will have no effect.
+        Avoid <literal>home-assistant.overridePythonAttrs</literal> if you use <literal>autoExtraComponents</literal>.
       '';
     };
 
@@ -238,22 +245,86 @@ in {
         rm -f "${cfg.configDir}/ui-lovelace.yaml"
         ln -s ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
       '');
-      serviceConfig = {
-        ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
+      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"
+        ]));
+      in {
+        ExecStart = "${package}/bin/hass --runner --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 = [
+          "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;
-        KillSignal = "SIGINT";
-        PrivateTmp = true;
-        RemoveIPC = true;
-        AmbientCapabilities = "cap_net_raw,cap_net_admin+eip";
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_NETLINK"
+          "AF_UNIX"
+        ] ++ optionals (useComponent "bluetooth_tracker" || useComponent "bluetooth_le_tracker") [
+          "AF_BLUETOOTH"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SupplementaryGroups = [ "dialout" ];
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+        ];
+        UMask = "0077";
       };
       path = [
         "/run/wrappers" # needed for ping
@@ -271,7 +342,6 @@ in {
       home = cfg.configDir;
       createHome = true;
       group = "hass";
-      extraGroups = [ "dialout" ];
       uid = config.ids.uids.hass;
     };
 
diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix
index 6a47dc3628f..c1b45864041 100644
--- a/nixos/modules/services/misc/jellyfin.nix
+++ b/nixos/modules/services/misc/jellyfin.nix
@@ -18,6 +18,7 @@ in
 
       package = mkOption {
         type = types.package;
+        default = pkgs.jellyfin;
         example = literalExample "pkgs.jellyfin";
         description = ''
           Jellyfin package to use.
@@ -29,6 +30,16 @@ in
         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.
+        '';
+      };
     };
   };
 
@@ -88,11 +99,6 @@ in
       };
     };
 
-    services.jellyfin.package = mkDefault (
-      if versionAtLeast config.system.stateVersion "20.09" then pkgs.jellyfin
-        else pkgs.jellyfin_10_5
-    );
-
     users.users = mkIf (cfg.user == "jellyfin") {
       jellyfin = {
         group = cfg.group;
@@ -104,6 +110,12 @@ in
       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/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/mame.nix b/nixos/modules/services/misc/mame.nix
index c5d5e9e4837..34a471ea4fe 100644
--- a/nixos/modules/services/misc/mame.nix
+++ b/nixos/modules/services/misc/mame.nix
@@ -53,7 +53,7 @@ in
       description = "MAME TUN/TAP Ethernet interface";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.iproute ];
+      path = [ pkgs.iproute2 ];
       serviceConfig = {
         Type = "oneshot";
         RemainAfterExit = true;
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..a0a5973d30f
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-appservice-irc.nix
@@ -0,0 +1,229 @@
+{ 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.runCommandNoCC "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;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/matrix-dendrite.nix b/nixos/modules/services/misc/matrix-dendrite.nix
new file mode 100644
index 00000000000..b719df29c5a
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-dendrite.nix
@@ -0,0 +1,181 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.matrix-dendrite;
+  settingsFormat = pkgs.formats.yaml { };
+  configurationYaml = settingsFormat.generate "dendrite.yaml" cfg.settings;
+  workingDir = "/var/lib/matrix-dendrite";
+in
+{
+  options.services.matrix-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/matrix-dendrite/server.cert";
+      default = null;
+      description = ''
+        The path to the TLS certificate.
+
+        <programlisting>
+          nix-shell -p matrix-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/matrix-dendrite/server.key";
+      default = null;
+      description = ''
+        The path to the TLS key.
+
+        <programlisting>
+          nix-shell -p matrix-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/matrix-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.matrix-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 matrix-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.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.
+            '';
+          };
+        };
+      };
+      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 matrix-dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
+      '';
+    }];
+
+    systemd.services.matrix-dendrite = {
+      description = "Dendrite Matrix homeserver";
+      after = [
+        "network.target"
+      ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "simple";
+        DynamicUser = true;
+        StateDirectory = "matrix-dendrite";
+        WorkingDirectory = workingDir;
+        RuntimeDirectory = "matrix-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/matrix-dendrite/dendrite.yaml
+          '' else ''
+            ${pkgs.coreutils}/bin/cp ${configurationYaml} /run/matrix-dendrite/dendrite.yaml
+          '';
+        ExecStart = lib.strings.concatStringsSep " " ([
+          "${pkgs.matrix-dendrite}/bin/dendrite-monolith-server"
+          "--config /run/matrix-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/matrix-synapse.xml b/nixos/modules/services/misc/matrix-synapse.xml
index 358b631eb48..41a56df0f2b 100644
--- a/nixos/modules/services/misc/matrix-synapse.xml
+++ b/nixos/modules/services/misc/matrix-synapse.xml
@@ -33,11 +33,11 @@
    <link xlink:href="https://github.com/matrix-org/synapse#synapse-installation">
    installation instructions of Synapse </link>.
 <programlisting>
-{ pkgs, ... }:
+{ pkgs, lib, ... }:
 let
   fqdn =
     let
-      join = hostName: domain: hostName + optionalString (domain != null) ".${domain}";
+      join = hostName: domain: hostName + lib.optionalString (domain != null) ".${domain}";
     in join config.networking.hostName config.networking.domain;
 in {
   networking = {
@@ -132,7 +132,7 @@ in {
       }
     ];
   };
-};
+}
 </programlisting>
   </para>
 
diff --git a/nixos/modules/services/misc/mautrix-telegram.nix b/nixos/modules/services/misc/mautrix-telegram.nix
index caeb4b04164..0ae5797fea0 100644
--- a/nixos/modules/services/misc/mautrix-telegram.nix
+++ b/nixos/modules/services/misc/mautrix-telegram.nix
@@ -6,8 +6,9 @@ let
   dataDir = "/var/lib/mautrix-telegram";
   registrationFile = "${dataDir}/telegram-registration.yaml";
   cfg = config.services.mautrix-telegram;
-  # TODO: switch to configGen.json once RFC42 is implemented
-  settingsFile = pkgs.writeText "mautrix-telegram-settings.json" (builtins.toJSON cfg.settings);
+  settingsFormat = pkgs.formats.json {};
+  settingsFileUnsubstituted = settingsFormat.generate "mautrix-telegram-config-unsubstituted.json" cfg.settings;
+  settingsFile = "${dataDir}/config.json";
 
 in {
   options = {
@@ -15,9 +16,8 @@ in {
       enable = mkEnableOption "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge";
 
       settings = mkOption rec {
-        # TODO: switch to types.config.json as prescribed by RFC42 once it's implemented
-        type = types.attrs;
         apply = recursiveUpdate default;
+        inherit (settingsFormat) type;
         default = {
           appservice = rec {
             database = "sqlite:///${dataDir}/mautrix-telegram.db";
@@ -124,6 +124,16 @@ in {
       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 0277
+        ${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 \
@@ -159,6 +169,8 @@ in {
             --config='${settingsFile}'
         '';
       };
+
+      restartTriggers = [ settingsFileUnsubstituted ];
     };
   };
 
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index 64bdbf159d5..133e96da0ec 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -21,6 +21,7 @@ let
          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" ];
     };
diff --git a/nixos/modules/services/misc/nix-gc.nix b/nixos/modules/services/misc/nix-gc.nix
index 12bed05757a..a7a6a3b5964 100644
--- a/nixos/modules/services/misc/nix-gc.nix
+++ b/nixos/modules/services/misc/nix-gc.nix
@@ -21,13 +21,45 @@ in
       };
 
       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 = ''
-          Specification (in the format described by
+          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>) of the time at
-          which the garbage collector will run.
+          <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.
         '';
       };
 
@@ -50,11 +82,18 @@ in
 
   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.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/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/packagekit.nix b/nixos/modules/services/misc/packagekit.nix
index 325c4e84e0d..93bd206bd98 100644
--- a/nixos/modules/services/misc/packagekit.nix
+++ b/nixos/modules/services/misc/packagekit.nix
@@ -1,55 +1,60 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
-
   cfg = config.services.packagekit;
 
-  packagekitConf = ''
-    [Daemon]
-    DefaultBackend=${cfg.backend}
-    KeepCache=false
-  '';
+  inherit (lib)
+    mkEnableOption mkOption mkIf mkRemovedOptionModule types
+    listToAttrs recursiveUpdate;
 
-  vendorConf = ''
-    [PackagesNotFound]
-    DefaultUrl=https://github.com/NixOS/nixpkgs
-    CodecUrl=https://github.com/NixOS/nixpkgs
-    HardwareUrl=https://github.com/NixOS/nixpkgs
-    FontUrl=https://github.com/NixOS/nixpkgs
-    MimeUrl=https://github.com/NixOS/nixpkgs
-  '';
+  iniFmt = pkgs.formats.ini { };
 
-in
+  confFiles = [
+    (iniFmt.generate "PackageKit.conf" (recursiveUpdate
+      {
+        Daemon = {
+          DefaultBackend = "test_nop";
+          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" ] "The only backend that doesn't blow up is `test_nop`.")
+  ];
 
-  options = {
+  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.
+    '';
 
-    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";
+    };
 
-      # TODO: integrate with PolicyKit if the nix backend matures to the point
-      # where it will require elevated permissions
-      backend = mkOption {
-        type = types.enum [ "test_nop" ];
-        default = "test_nop";
-        description = ''
-          PackageKit supports multiple different backends and <literal>auto</literal> which
-          should do the right thing.
-          </para>
-          <para>
-          On NixOS however, we do not have a backend compatible with nix 2.0
-          (refer to <link xlink:href="https://github.com/NixOS/nix/issues/233">this issue</link> so we have to force
-          it to <literal>test_nop</literal> for now.
-        '';
-      };
+    vendorSettings = mkOption {
+      type = iniFmt.type;
+      default = { };
+      description = "Additional settings passed straight through to Vendor.conf";
     };
   };
 
@@ -59,7 +64,9 @@ in
 
     systemd.packages = with pkgs; [ packagekit ];
 
-    environment.etc."PackageKit/PackageKit.conf".text = packagekitConf;
-    environment.etc."PackageKit/Vendor.conf".text = vendorConf;
+    environment.etc = listToAttrs (map
+      (e:
+        lib.nameValuePair "PackageKit/${e.name}" { source = e; })
+      confFiles);
   };
 }
diff --git a/nixos/modules/services/misc/pinnwand.nix b/nixos/modules/services/misc/pinnwand.nix
index aa1ee5cfaa7..cbc796c9a7c 100644
--- a/nixos/modules/services/misc/pinnwand.nix
+++ b/nixos/modules/services/misc/pinnwand.nix
@@ -24,55 +24,80 @@ in
         Your <filename>pinnwand.toml</filename> as a Nix attribute set. Look up
         possible options in the <link xlink:href="https://github.com/supakeen/pinnwand/blob/master/pinnwand.toml-example">pinnwand.toml-example</link>.
       '';
-      default = {
-        # https://github.com/supakeen/pinnwand/blob/master/pinnwand.toml-example
-        database_uri = "sqlite:///var/lib/pinnwand/pinnwand.db";
-        preferred_lexeres = [];
-        paste_size = 262144;
-        paste_help = ''
-          <p>Welcome to pinnwand, this site is a pastebin. It allows you to share code with others. If you write code in the text area below and press the paste button you will be given a link you can share with others so they can view your code as well.</p><p>People with the link can view your pasted code, only you can remove your paste and it expires automatically. Note that anyone could guess the URI to your paste so don't rely on it being private.</p>
-        '';
-        footer = ''
-          View <a href="//github.com/supakeen/pinnwand" target="_BLANK">source code</a>, the <a href="/removal">removal</a> or <a href="/expiry">expiry</a> stories, or read the <a href="/about">about</a> page.
-        '';
-      };
+      default = {};
     };
   };
 
   config = mkIf cfg.enable {
-    systemd.services.pinnwand = {
-      description = "Pinnwannd HTTP Server";
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
+    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;
 
-      unitConfig.Documentation = "https://pinnwand.readthedocs.io/en/latest/";
-      serviceConfig = {
-        ExecStart = "${pkgs.pinnwand}/bin/pinnwand --configuration-path ${configFile} http --port ${toString(cfg.port)}";
         StateDirectory = "pinnwand";
         StateDirectoryMode = "0700";
 
         AmbientCapabilities = [];
         CapabilityBoundingSet = "";
         DevicePolicy = "closed";
-        DynamicUser = true;
         LockPersonality = true;
         MemoryDenyWriteExecute = true;
         PrivateDevices = true;
         PrivateUsers = true;
+        ProcSubset = "pid";
         ProtectClock = true;
         ProtectControlGroups = true;
-        ProtectKernelLogs = true;
         ProtectHome = true;
         ProtectHostname = true;
+        ProtectKernelLogs = true;
         ProtectKernelModules = true;
         ProtectKernelTunables = true;
-        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        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/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/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 8b53eb471db..e0055576d6f 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -28,7 +28,7 @@ let
   unpack = id: (name: source:
     pkgs.stdenv.mkDerivation {
       name = "redmine-${id}-${name}";
-      buildInputs = [ pkgs.unzip ];
+      nativeBuildInputs = [ pkgs.unzip ];
       buildCommand = ''
         mkdir -p $out
         cd $out
diff --git a/nixos/modules/services/misc/zigbee2mqtt.nix b/nixos/modules/services/misc/zigbee2mqtt.nix
index cd987eb76c7..4458da1346b 100644
--- a/nixos/modules/services/misc/zigbee2mqtt.nix
+++ b/nixos/modules/services/misc/zigbee2mqtt.nix
@@ -5,29 +5,17 @@ with lib;
 let
   cfg = config.services.zigbee2mqtt;
 
-  configJSON = pkgs.writeText "configuration.json"
-    (builtins.toJSON (recursiveUpdate defaultConfig cfg.config));
-  configFile = pkgs.runCommand "configuration.yaml" { preferLocalBuild = true; } ''
-    ${pkgs.remarshal}/bin/json2yaml -i ${configJSON} -o $out
-  '';
+  format = pkgs.formats.yaml { };
+  configFile = format.generate "zigbee2mqtt.yaml" cfg.settings;
 
-  # the default config contains all required settings,
-  # so the service starts up without crashing.
-  defaultConfig = {
-    homeassistant = false;
-    permit_join = false;
-    mqtt = {
-      base_topic = "zigbee2mqtt";
-      server = "mqtt://localhost:1883";
-    };
-    serial.port = "/dev/ttyACM0";
-    # put device configuration into separate file because configuration.yaml
-    # is copied from the store on startup
-    devices = "devices.yaml";
-  };
 in
 {
-  meta.maintainers = with maintainers; [ sweber ];
+  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";
@@ -37,7 +25,11 @@ in
       default = pkgs.zigbee2mqtt.override {
         dataDir = cfg.dataDir;
       };
-      defaultText = "pkgs.zigbee2mqtt";
+      defaultText = literalExample ''
+        pkgs.zigbee2mqtt {
+          dataDir = services.zigbee2mqtt.dataDir
+        }
+      '';
       type = types.package;
     };
 
@@ -47,9 +39,9 @@ in
       type = types.path;
     };
 
-    config = mkOption {
+    settings = mkOption {
+      type = format.type;
       default = {};
-      type = with types; nullOr attrs;
       example = literalExample ''
         {
           homeassistant = config.services.home-assistant.enable;
@@ -61,11 +53,28 @@ in
       '';
       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" ];
@@ -76,10 +85,48 @@ in
         User = "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;
-        PrivateTmp = true;
         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"
@@ -90,7 +137,6 @@ in
       home = cfg.dataDir;
       createHome = true;
       group = "zigbee2mqtt";
-      extraGroups = [ "dialout" ];
       uid = config.ids.uids.zigbee2mqtt;
     };
 
diff --git a/nixos/modules/services/monitoring/alerta.nix b/nixos/modules/services/monitoring/alerta.nix
index 34f2d41706a..7c6eff713cb 100644
--- a/nixos/modules/services/monitoring/alerta.nix
+++ b/nixos/modules/services/monitoring/alerta.nix
@@ -95,13 +95,13 @@ in
         ALERTA_SVR_CONF_FILE = alertaConf;
       };
       serviceConfig = {
-        ExecStart = "${pkgs.python36Packages.alerta-server}/bin/alertad run --port ${toString cfg.port} --host ${cfg.bind}";
+        ExecStart = "${pkgs.alerta-server}/bin/alertad run --port ${toString cfg.port} --host ${cfg.bind}";
         User = "alerta";
         Group = "alerta";
       };
     };
 
-    environment.systemPackages = [ pkgs.python36Packages.alerta ];
+    environment.systemPackages = [ pkgs.alerta ];
 
     users.users.alerta = {
       uid = config.ids.uids.alerta;
diff --git a/nixos/modules/services/monitoring/datadog-agent.nix b/nixos/modules/services/monitoring/datadog-agent.nix
index d97565f15d6..b25a53435d0 100644
--- a/nixos/modules/services/monitoring/datadog-agent.nix
+++ b/nixos/modules/services/monitoring/datadog-agent.nix
@@ -225,7 +225,7 @@ in {
     };
   };
   config = mkIf cfg.enable {
-    environment.systemPackages = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute ];
+    environment.systemPackages = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute2 ];
 
     users.users.datadog = {
       description = "Datadog Agent User";
@@ -239,7 +239,7 @@ in {
 
     systemd.services = let
       makeService = attrs: recursiveUpdate {
-        path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps pkgs.iproute ];
+        path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps pkgs.iproute2 ];
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
           User = "datadog";
diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix
index c8515c4b898..86e306ab404 100644
--- a/nixos/modules/services/monitoring/grafana.nix
+++ b/nixos/modules/services/monitoring/grafana.nix
@@ -15,6 +15,7 @@ let
     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;
@@ -65,10 +66,18 @@ let
 
   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}
+    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:
@@ -79,80 +88,80 @@ let
     options = {
       name = mkOption {
         type = types.str;
-        description = "Name of the datasource. Required";
+        description = "Name of the datasource. Required.";
       };
       type = mkOption {
         type = types.enum ["graphite" "prometheus" "cloudwatch" "elasticsearch" "influxdb" "opentsdb" "mysql" "mssql" "postgres" "loki"];
-        description = "Datasource type. Required";
+        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";
+        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";
+        description = "Org id. will default to orgId 1 if not specified.";
       };
       url = mkOption {
         type = types.str;
-        description = "Url of the datasource";
+        description = "Url of the datasource.";
       };
       password = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "Database password, if used";
+        description = "Database password, if used.";
       };
       user = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "Database user, if used";
+        description = "Database user, if used.";
       };
       database = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "Database name, if used";
+        description = "Database name, if used.";
       };
       basicAuth = mkOption {
         type = types.nullOr types.bool;
         default = null;
-        description = "Enable/disable basic auth";
+        description = "Enable/disable basic auth.";
       };
       basicAuthUser = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "Basic auth username";
+        description = "Basic auth username.";
       };
       basicAuthPassword = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "Basic auth password";
+        description = "Basic auth password.";
       };
       withCredentials = mkOption {
         type = types.bool;
         default = false;
-        description = "Enable/disable with credentials headers";
+        description = "Enable/disable with credentials headers.";
       };
       isDefault = mkOption {
         type = types.bool;
         default = false;
-        description = "Mark as default datasource. Max one per org";
+        description = "Mark as default datasource. Max one per org.";
       };
       jsonData = mkOption {
         type = types.nullOr types.attrs;
         default = null;
-        description = "Datasource specific configuration";
+        description = "Datasource specific configuration.";
       };
       secureJsonData = mkOption {
         type = types.nullOr types.attrs;
         default = null;
-        description = "Datasource specific secure configuration";
+        description = "Datasource specific secure configuration.";
       };
       version = mkOption {
         type = types.int;
         default = 1;
-        description = "Version";
+        description = "Version.";
       };
       editable = mkOption {
         type = types.bool;
@@ -168,41 +177,99 @@ let
       name = mkOption {
         type = types.str;
         default = "default";
-        description = "Provider name";
+        description = "Provider name.";
       };
       orgId = mkOption {
         type = types.int;
         default = 1;
-        description = "Organization ID";
+        description = "Organization ID.";
       };
       folder = mkOption {
         type = types.str;
         default = "";
-        description = "Add dashboards to the specified folder";
+        description = "Add dashboards to the specified folder.";
       };
       type = mkOption {
         type = types.str;
         default = "file";
-        description = "Dashboard provider type";
+        description = "Dashboard provider type.";
       };
       disableDeletion = mkOption {
         type = types.bool;
         default = false;
-        description = "Disable deletion when JSON file is removed";
+        description = "Disable deletion when JSON file is removed.";
       };
       updateIntervalSeconds = mkOption {
         type = types.int;
         default = 10;
-        description = "How often Grafana will scan for changed dashboards";
+        description = "How often Grafana will scan for changed dashboards.";
       };
       options = {
         path = mkOption {
           type = types.path;
-          description = "Path grafana will watch for dashboards";
+          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";
@@ -225,6 +292,12 @@ in {
       type = types.int;
     };
 
+    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";
@@ -337,17 +410,23 @@ in {
     provision = {
       enable = mkEnableOption "provision";
       datasources = mkOption {
-        description = "Grafana datasources configuration";
+        description = "Grafana datasources configuration.";
         default = [];
         type = types.listOf grafanaTypes.datasourceConfig;
         apply = x: map _filter x;
       };
       dashboards = mkOption {
-        description = "Grafana dashboard configuration";
+        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 = {
@@ -391,12 +470,12 @@ in {
     smtp = {
       enable = mkEnableOption "smtp";
       host = mkOption {
-        description = "Host to connect to";
+        description = "Host to connect to.";
         default = "localhost:25";
         type = types.str;
       };
       user = mkOption {
-        description = "User used for authentication";
+        description = "User used for authentication.";
         default = "";
         type = types.str;
       };
@@ -417,7 +496,7 @@ in {
         type = types.nullOr types.path;
       };
       fromAddress = mkOption {
-        description = "Email address used for sending";
+        description = "Email address used for sending.";
         default = "admin@grafana.localhost";
         type = types.str;
       };
@@ -425,7 +504,7 @@ in {
 
     users = {
       allowSignUp = mkOption {
-        description = "Disable user signup / registration";
+        description = "Disable user signup / registration.";
         default = false;
         type = types.bool;
       };
@@ -451,17 +530,17 @@ in {
 
     auth.anonymous = {
       enable = mkOption {
-        description = "Whether to allow anonymous access";
+        description = "Whether to allow anonymous access.";
         default = false;
         type = types.bool;
       };
       org_name = mkOption {
-        description = "Which organization to allow anonymous access to";
+        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";
+        description = "Which role anonymous users have in the organization.";
         default = "Viewer";
         type = types.str;
       };
@@ -470,7 +549,7 @@ in {
 
     analytics.reporting = {
       enable = mkOption {
-        description = "Whether to allow anonymous usage reporting to stats.grafana.net";
+        description = "Whether to allow anonymous usage reporting to stats.grafana.net.";
         default = true;
         type = types.bool;
       };
@@ -496,6 +575,9 @@ in {
       (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 ];
@@ -547,6 +629,8 @@ in {
       serviceConfig = {
         WorkingDirectory = cfg.dataDir;
         User = "grafana";
+        RuntimeDirectory = "grafana";
+        RuntimeDirectoryMode = "0755";
       };
       preStart = ''
         ln -fs ${cfg.package}/share/grafana/conf ${cfg.dataDir}
diff --git a/nixos/modules/services/monitoring/nagios.nix b/nixos/modules/services/monitoring/nagios.nix
index 9ac6869068f..61214508a9c 100644
--- a/nixos/modules/services/monitoring/nagios.nix
+++ b/nixos/modules/services/monitoring/nagios.nix
@@ -192,6 +192,7 @@ in
       path     = [ pkgs.nagios ] ++ cfg.plugins;
       wantedBy = [ "multi-user.target" ];
       after    = [ "network.target" ];
+      restartTriggers = [ nagiosCfgFile ];
 
       serviceConfig = {
         User = "nagios";
@@ -201,7 +202,6 @@ in
         LogsDirectory = "nagios";
         StateDirectory = "nagios";
         ExecStart = "${pkgs.nagios}/bin/nagios /etc/nagios.cfg";
-        X-ReloadIfChanged = nagiosCfgFile;
       };
     };
 
diff --git a/nixos/modules/services/monitoring/netdata.nix b/nixos/modules/services/monitoring/netdata.nix
index d5b679097b3..c2ee1c0df7f 100644
--- a/nixos/modules/services/monitoring/netdata.nix
+++ b/nixos/modules/services/monitoring/netdata.nix
@@ -123,9 +123,20 @@ in {
             "error log" = "syslog";
           };
         '';
-        };
+      };
+
+      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 =
@@ -138,10 +149,15 @@ in {
       description = "Real time performance monitoring";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = (with pkgs; [ curl gawk which ]) ++ lib.optional cfg.python.enable
-        (pkgs.python3.withPackages cfg.python.extraPackages);
+      path = (with pkgs; [ curl gawk iproute2 which ])
+        ++ 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";
+      };
       serviceConfig = {
-        Environment="PYTHONPATH=${cfg.package}/libexec/netdata/python.d/python_modules";
         ExecStart = "${cfg.package}/bin/netdata -P /run/netdata/netdata.pid -D -c ${configFile}";
         ExecReload = "${pkgs.util-linux}/bin/kill -s HUP -s USR1 -s USR2 $MAINPID";
         TimeoutStopSec = 60;
@@ -167,6 +183,9 @@ in {
         ConfigurationDirectory = "netdata";
         ConfigurationDirectoryMode = "0755";
         # Capabilities
+        AmbientCapabilities = [
+          "CAP_SETUID"            # is required for cgroups and cgroups-network plugins
+        ];
         CapabilityBoundingSet = [
           "CAP_DAC_OVERRIDE"      # is required for freeipmi and slabinfo plugins
           "CAP_DAC_READ_SEARCH"   # is required for apps plugin
@@ -176,6 +195,8 @@ in {
           "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";
diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix
index bd74e1a9cdb..1d483627e9e 100644
--- a/nixos/modules/services/monitoring/prometheus/default.nix
+++ b/nixos/modules/services/monitoring/prometheus/default.nix
@@ -112,7 +112,7 @@ let
           http://tools.ietf.org/html/rfc4366#section-3.1
         '';
       };
-      name = mkOpt types.string ''
+      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.
@@ -174,7 +174,7 @@ let
       write_relabel_configs = mkOpt (types.listOf promTypes.relabel_config) ''
         List of remote write relabel configurations.
       '';
-      name = mkOpt types.string ''
+      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.
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index 940f2818937..ce7c215fd14 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -3,7 +3,7 @@
 let
   inherit (lib) concatStrings foldl foldl' genAttrs literalExample maintainers
                 mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption
-                optional types;
+                optional types mkOptionDefault flip attrNames;
 
   cfg = config.services.prometheus.exporters;
 
@@ -22,15 +22,20 @@ let
 
   exporterOpts = genAttrs [
     "apcupsd"
+    "artifactory"
     "bind"
     "bird"
+    "bitcoin"
     "blackbox"
     "collectd"
     "dnsmasq"
+    "domain"
     "dovecot"
     "fritzbox"
     "json"
+    "jitsi"
     "keylight"
+    "knot"
     "lnd"
     "mail"
     "mikrotik"
@@ -40,6 +45,7 @@ let
     "nginx"
     "nginxlog"
     "node"
+    "openldap"
     "openvpn"
     "postfix"
     "postgres"
@@ -51,7 +57,9 @@ let
     "smokeping"
     "sql"
     "surfboard"
+    "systemd"
     "tor"
+    "unbound"
     "unifi"
     "unifi-poller"
     "varnish"
@@ -64,7 +72,7 @@ let
   mkExporterOpts = ({ name, port }: {
     enable = mkEnableOption "the prometheus ${name} exporter";
     port = mkOption {
-      type = types.int;
+      type = types.port;
       default = port;
       description = ''
         Port to listen on.
@@ -92,9 +100,8 @@ let
       '';
     };
     firewallFilter = mkOption {
-      type = types.str;
-      default = "-p tcp -m tcp --dport ${toString cfg.${name}.port}";
-      defaultText = "-p tcp -m tcp --dport ${toString port}";
+      type = types.nullOr types.str;
+      default = null;
       example = literalExample ''
         "-i eth0 -p tcp -m tcp --dport ${toString port}"
       '';
@@ -122,12 +129,14 @@ let
 
   mkSubModule = { name, port, extraOpts, imports }: {
     ${name} = mkOption {
-      type = types.submodule {
+      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 = {};
     };
@@ -232,7 +241,13 @@ in
         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;
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/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/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/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/knot.nix b/nixos/modules/services/monitoring/prometheus/exporters/knot.nix
new file mode 100644
index 00000000000..46c28fe0a57
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/knot.nix
@@ -0,0 +1,50 @@
+{ 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 = "\${pkgs.knot-dns}/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" ];
+    };
+  };
+}
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/postgres.nix b/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix
index 1ece73a1159..dd3bec8ec16 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix
@@ -30,12 +30,49 @@ in
         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} \
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..0514469b8a6
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix
@@ -0,0 +1,18 @@
+{ 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}
+      '';
+    };
+  };
+}
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..56a559531c1
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix
@@ -0,0 +1,59 @@
+{ 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}
+      '';
+    };
+  }] ++ [
+    (mkIf config.services.unbound.enable {
+      after = [ "unbound.service" ];
+      requires = [ "unbound.service" ];
+    })
+  ]);
+}
diff --git a/nixos/modules/services/monitoring/scollector.nix b/nixos/modules/services/monitoring/scollector.nix
index 6f13ce889cb..ef535585e9b 100644
--- a/nixos/modules/services/monitoring/scollector.nix
+++ b/nixos/modules/services/monitoring/scollector.nix
@@ -113,7 +113,7 @@ in {
       description = "scollector metrics collector (part of Bosun)";
       wantedBy = [ "multi-user.target" ];
 
-      path = [ pkgs.coreutils pkgs.iproute ];
+      path = [ pkgs.coreutils pkgs.iproute2 ];
 
       serviceConfig = {
         User = cfg.user;
diff --git a/nixos/modules/services/monitoring/tuptime.nix b/nixos/modules/services/monitoring/tuptime.nix
index 8f79d916599..17c5c1f56ea 100644
--- a/nixos/modules/services/monitoring/tuptime.nix
+++ b/nixos/modules/services/monitoring/tuptime.nix
@@ -34,7 +34,10 @@ in {
 
     users = {
       groups._tuptime.members = [ "_tuptime" ];
-      users._tuptime.description = "tuptime database owner";
+      users._tuptime = {
+        isSystemUser = true;
+        description = "tuptime database owner";
+      };
     };
 
     systemd = {
diff --git a/nixos/modules/services/monitoring/vnstat.nix b/nixos/modules/services/monitoring/vnstat.nix
index e9bedb704a4..5e19c399568 100644
--- a/nixos/modules/services/monitoring/vnstat.nix
+++ b/nixos/modules/services/monitoring/vnstat.nix
@@ -6,21 +6,21 @@ let
   cfg = config.services.vnstat;
 in {
   options.services.vnstat = {
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Whether to enable update of network usage statistics via vnstatd.
-      '';
-    };
+    enable = mkEnableOption "update of network usage statistics via vnstatd";
   };
 
   config = mkIf cfg.enable {
-    users.users.vnstatd = {
-      isSystemUser = true;
-      description = "vnstat daemon user";
-      home = "/var/lib/vnstat";
-      createHome = true;
+
+    environment.systemPackages = [ pkgs.vnstat ];
+
+    users = {
+      groups.vnstatd = {};
+
+      users.vnstatd = {
+        isSystemUser = true;
+        group = "vnstatd";
+        description = "vnstat daemon user";
+      };
     };
 
     systemd.services.vnstat = {
@@ -33,7 +33,6 @@ in {
         "man:vnstat(1)"
         "man:vnstat.conf(5)"
       ];
-      preStart = "chmod 755 /var/lib/vnstat";
       serviceConfig = {
         ExecStart = "${pkgs.vnstat}/bin/vnstatd -n";
         ExecReload = "${pkgs.procps}/bin/kill -HUP $MAINPID";
@@ -52,7 +51,10 @@ in {
         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
index 73eed7aa66a..e7dd9e3393d 100644
--- a/nixos/modules/services/monitoring/zabbix-agent.nix
+++ b/nixos/modules/services/monitoring/zabbix-agent.nix
@@ -128,11 +128,16 @@ in
       {
         LogType = "console";
         Server = cfg.server;
-        ListenIP = cfg.listen.ip;
         ListenPort = cfg.listen.port;
-        LoadModule = builtins.attrNames cfg.modules;
       }
-      (mkIf (cfg.modules != {}) { LoadModulePath = "${moduleEnv}/lib"; })
+      (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 {
diff --git a/nixos/modules/services/network-filesystems/ceph.nix b/nixos/modules/services/network-filesystems/ceph.nix
index 632c3fb1059..d833062c473 100644
--- a/nixos/modules/services/network-filesystems/ceph.nix
+++ b/nixos/modules/services/network-filesystems/ceph.nix
@@ -316,7 +316,7 @@ in
     client = {
       enable = mkEnableOption "Ceph client configuration";
       extraConfig = mkOption {
-        type = with types; attrsOf str;
+        type = with types; attrsOf (attrsOf str);
         default = {};
         example = ''
           {
diff --git a/nixos/modules/services/network-filesystems/ipfs.nix b/nixos/modules/services/network-filesystems/ipfs.nix
index 2082d513161..6d8dfcce933 100644
--- a/nixos/modules/services/network-filesystems/ipfs.nix
+++ b/nixos/modules/services/network-filesystems/ipfs.nix
@@ -216,14 +216,11 @@ in {
 
     systemd.packages = [ cfg.package ];
 
-    systemd.services.ipfs-init = {
-      description = "IPFS Initializer";
-
+    systemd.services.ipfs = {
+      path = [ "/run/wrappers" cfg.package ];
       environment.IPFS_PATH = cfg.dataDir;
 
-      path = [ cfg.package ];
-
-      script = ''
+      preStart = ''
         if [[ ! -f ${cfg.dataDir}/config ]]; then
           ipfs init ${optionalString cfg.emptyRepo "-e"} \
             ${optionalString (! cfg.localDiscovery) "--profile=server"}
@@ -233,26 +230,7 @@ in {
             else "ipfs config profile apply server"
           }
         fi
-      '';
-
-      wantedBy = [ "default.target" ];
-
-      serviceConfig = {
-        Type = "oneshot";
-        RemainAfterExit = true;
-        User = cfg.user;
-        Group = cfg.group;
-      };
-    };
-
-    systemd.services.ipfs = {
-      path = [ "/run/wrappers" cfg.package ];
-      environment.IPFS_PATH = cfg.dataDir;
-
-      wants = [ "ipfs-init.service" ];
-      after = [ "ipfs-init.service" ];
-
-      preStart = optionalString cfg.autoMount ''
+      '' + optionalString cfg.autoMount ''
         ipfs --local config Mounts.FuseAllowOther --json true
         ipfs --local config Mounts.IPFS ${cfg.ipfsMountDir}
         ipfs --local config Mounts.IPNS ${cfg.ipnsMountDir}
@@ -296,7 +274,7 @@ in {
 
     systemd.sockets.ipfs-api = {
       wantedBy = [ "sockets.target" ];
-      # We also include "%t/ipfs.sock" because tere is no way to put the "%t"
+      # 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;
diff --git a/nixos/modules/services/network-filesystems/samba.nix b/nixos/modules/services/network-filesystems/samba.nix
index d6e2904b3c3..78ea245cb35 100644
--- a/nixos/modules/services/network-filesystems/samba.nix
+++ b/nixos/modules/services/network-filesystems/samba.nix
@@ -156,7 +156,6 @@ in
       securityType = mkOption {
         type = types.str;
         default = "user";
-        example = "share";
         description = "Samba security type";
       };
 
diff --git a/nixos/modules/services/networking/adguardhome.nix b/nixos/modules/services/networking/adguardhome.nix
new file mode 100644
index 00000000000..4388ef2b7e5
--- /dev/null
+++ b/nixos/modules/services/networking/adguardhome.nix
@@ -0,0 +1,78 @@
+{ 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"
+    "--host ${cfg.host}"
+    "--port ${toString cfg.port}"
+  ] ++ cfg.extraArgs);
+
+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.
+      '';
+    };
+
+    extraArgs = mkOption {
+      default = [ ];
+      type = listOf str;
+      description = ''
+        Extra command line parameters to be passed to the adguardhome binary.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.adguardhome = {
+      description = "AdGuard Home: Network-level blocker";
+      after = [ "syslog.target" "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      unitConfig = {
+        StartLimitIntervalSec = 5;
+        StartLimitBurst = 10;
+      };
+      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/babeld.nix b/nixos/modules/services/networking/babeld.nix
index 272c58ecd7f..5e14283179a 100644
--- a/nixos/modules/services/networking/babeld.nix
+++ b/nixos/modules/services/networking/babeld.nix
@@ -19,7 +19,10 @@ let
     "interface ${name} ${paramsString interface}\n";
 
   configFile = with cfg; pkgs.writeText "babeld.conf" (
-    (optionalString (cfg.interfaceDefaults != null) ''
+    ''
+      skip-kernel-setup true
+    ''
+    + (optionalString (cfg.interfaceDefaults != null) ''
       default ${paramsString cfg.interfaceDefaults}
     '')
     + (concatMapStrings interfaceConfig (attrNames cfg.interfaces))
@@ -29,6 +32,8 @@ in
 
 {
 
+  meta.maintainers = with maintainers; [ hexa ];
+
   ###### interface
 
   options = {
@@ -84,13 +89,22 @@ in
 
   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" ];
+        DynamicUser = true;
         IPAddressAllow = [ "fe80::/64" "ff00::/8" "::1/128" "127.0.0.0/8" ];
         IPAddressDeny = "any";
         LockPersonality = true;
@@ -98,11 +112,11 @@ in
         MemoryDenyWriteExecute = true;
         ProtectSystem = "strict";
         ProtectClock = true;
-        ProtectKernelTunables = false; # Couldn't write sysctl: Read-only file system
+        ProtectKernelTunables = true;
         ProtectKernelModules = true;
         ProtectKernelLogs = true;
         ProtectControlGroups = true;
-        RestrictAddressFamilies = [ "AF_NETLINK" "AF_INET6" ];
+        RestrictAddressFamilies = [ "AF_NETLINK" "AF_INET6" "AF_INET" ];
         RestrictNamespaces = true;
         RestrictRealtime = true;
         RestrictSUIDSGID = true;
diff --git a/nixos/modules/services/networking/bind.nix b/nixos/modules/services/networking/bind.nix
index e507e8ce9ee..b73b2b62685 100644
--- a/nixos/modules/services/networking/bind.nix
+++ b/nixos/modules/services/networking/bind.nix
@@ -8,32 +8,37 @@ let
 
   bindUser = "named";
 
-  bindZoneOptions = {
-    name = mkOption {
-      type = types.str;
-      description = "Name of the zone.";
-    };
-    master = mkOption {
-      description = "Master=false means slave server";
-      type = types.bool;
-    };
-    file = mkOption {
-      type = types.either types.str types.path;
-      description = "Zone file resource records contain columns of data, separated by whitespace, that define the record.";
-    };
-    masters = mkOption {
-      type = types.listOf types.str;
-      description = "List of servers for inclusion in stub and secondary zones.";
-    };
-    slaves = mkOption {
-      type = types.listOf types.str;
-      description = "Addresses who may request zone transfers.";
-      default = [];
-    };
-    extraConfig = mkOption {
-      type = types.str;
-      description = "Extra zone config to be appended at the end of the zone section.";
-      default = "";
+  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 = "";
+      };
     };
   };
 
@@ -84,7 +89,7 @@ let
                 ${extraConfig}
               };
             '')
-          cfg.zones }
+          (attrValues cfg.zones) }
     '';
 
 in
@@ -153,18 +158,19 @@ in
 
       zones = mkOption {
         default = [];
-        type = types.listOf (types.submodule [ { options = bindZoneOptions; } ]);
+        type = with types; coercedTo (listOf attrs) bindZoneCoerce (attrsOf (types.submodule bindZoneOptions));
         description = "
           List of zones we claim authority over.
         ";
-        example = [{
-          name = "example.com";
-          master = false;
-          file = "/var/dns/example.com";
-          masters = ["192.168.0.1"];
-          slaves = [];
-          extraConfig = "";
-        }];
+        example = {
+          "example.com" = {
+            master = false;
+            file = "/var/dns/example.com";
+            masters = ["192.168.0.1"];
+            slaves = [];
+            extraConfig = "";
+          };
+        };
       };
 
       extraConfig = mkOption {
diff --git a/nixos/modules/services/networking/bird.nix b/nixos/modules/services/networking/bird.nix
index 4ae35875c0f..1923afdf83f 100644
--- a/nixos/modules/services/networking/bird.nix
+++ b/nixos/modules/services/networking/bird.nix
@@ -1,7 +1,7 @@
 { config, lib, pkgs, ... }:
 
 let
-  inherit (lib) mkEnableOption mkIf mkOption types;
+  inherit (lib) mkEnableOption mkIf mkOption optionalString types;
 
   generic = variant:
     let
@@ -26,6 +26,14 @@ let
               <link xlink:href='http://bird.network.cz/'/>
             '';
           };
+          checkConfig = mkOption {
+            type = types.bool;
+            default = true;
+            description = ''
+              Whether the config should be checked at build time.
+              Disabling this might become necessary if the config includes files not present during build time.
+            '';
+          };
         };
       };
 
@@ -36,7 +44,7 @@ let
         environment.etc."bird/${variant}.conf".source = pkgs.writeTextFile {
           name = "${variant}.conf";
           text = cfg.config;
-          checkPhase = ''
+          checkPhase = optionalString cfg.checkConfig ''
             ${pkg}/bin/${birdBin} -d -p -c $out
           '';
         };
@@ -50,7 +58,7 @@ let
             Type = "forking";
             Restart = "on-failure";
             ExecStart = "${pkg}/bin/${birdBin} -c /etc/bird/${variant}.conf -u ${variant} -g ${variant}";
-            ExecReload = "${pkg}/bin/${birdc} configure";
+            ExecReload = "/bin/sh -c '${pkg}/bin/${birdBin} -c /etc/bird/${variant}.conf -p && ${pkg}/bin/${birdc} configure'";
             ExecStop = "${pkg}/bin/${birdc} down";
             CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_SETUID" "CAP_SETGID"
                                       # see bird/sysdep/linux/syspriv.h
@@ -65,6 +73,7 @@ let
           users.${variant} = {
             description = "BIRD Internet Routing Daemon user";
             group = variant;
+            isSystemUser = true;
           };
           groups.${variant} = {};
         };
diff --git a/nixos/modules/services/networking/cjdns.nix b/nixos/modules/services/networking/cjdns.nix
index f116d6392ea..e9a0e5af1a4 100644
--- a/nixos/modules/services/networking/cjdns.nix
+++ b/nixos/modules/services/networking/cjdns.nix
@@ -245,7 +245,7 @@ in
         fi
 
         if [ -z "$CJDNS_ADMIN_PASSWORD" ]; then
-            echo "CJDNS_ADMIN_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 96)" \
+            echo "CJDNS_ADMIN_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" \
                 >> /etc/cjdns.keys
         fi
       '';
diff --git a/nixos/modules/services/networking/consul.nix b/nixos/modules/services/networking/consul.nix
index bfaea4e167c..ae7998913ee 100644
--- a/nixos/modules/services/networking/consul.nix
+++ b/nixos/modules/services/networking/consul.nix
@@ -191,7 +191,7 @@ in
           ExecStop = "${cfg.package}/bin/consul leave";
         });
 
-        path = with pkgs; [ iproute gnugrep gawk consul ];
+        path = with pkgs; [ iproute2 gnugrep gawk consul ];
         preStart = ''
           mkdir -m 0700 -p ${dataDir}
           chown -R consul ${dataDir}
diff --git a/nixos/modules/services/networking/croc.nix b/nixos/modules/services/networking/croc.nix
new file mode 100644
index 00000000000..b218fab2196
--- /dev/null
+++ b/nixos/modules/services/networking/croc.nix
@@ -0,0 +1,88 @@
+{ 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 = "noaccess";
+        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" "~@chown" "~@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/dnsdist.nix b/nixos/modules/services/networking/dnsdist.nix
index 3584915d0aa..c7c6a79864c 100644
--- a/nixos/modules/services/networking/dnsdist.nix
+++ b/nixos/modules/services/networking/dnsdist.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg = config.services.dnsdist;
-  configFile = pkgs.writeText "dndist.conf" ''
+  configFile = pkgs.writeText "dnsdist.conf" ''
     setLocal('${cfg.listenAddress}:${toString cfg.listenPort}')
     ${cfg.extraConfig}
   '';
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..0e55bc38665
--- /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 = literalExample [ "--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/flannel.nix b/nixos/modules/services/networking/flannel.nix
index 4c040112d28..32a7eb3ed69 100644
--- a/nixos/modules/services/networking/flannel.nix
+++ b/nixos/modules/services/networking/flannel.nix
@@ -162,10 +162,7 @@ in {
         NODE_NAME = cfg.nodeName;
       };
       path = [ pkgs.iptables ];
-      preStart = ''
-        mkdir -p /run/flannel
-        touch /run/flannel/docker
-      '' + optionalString (cfg.storageBackend == "etcd") ''
+      preStart = optionalString (cfg.storageBackend == "etcd") ''
         echo "setting network configuration"
         until ${pkgs.etcdctl}/bin/etcdctl set /coreos.com/network/config '${builtins.toJSON networkConfig}'
         do
@@ -177,6 +174,7 @@ in {
         ExecStart = "${cfg.package}/bin/flannel";
         Restart = "always";
         RestartSec = "10s";
+        RuntimeDirectory = "flannel";
       };
     };
 
diff --git a/nixos/modules/services/networking/gobgpd.nix b/nixos/modules/services/networking/gobgpd.nix
new file mode 100644
index 00000000000..d3b03471f4e
--- /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 = literalExample ''
+        {
+          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
index b851facf1e3..4fad37ba15e 100644
--- a/nixos/modules/services/networking/gvpe.nix
+++ b/nixos/modules/services/networking/gvpe.nix
@@ -27,7 +27,7 @@ let
     text = ''
       #! /bin/sh
 
-      export PATH=$PATH:${pkgs.iproute}/sbin
+      export PATH=$PATH:${pkgs.iproute2}/sbin
 
       ip link set $IFNAME up
       ip address add ${cfg.ipAddress} dev $IFNAME
diff --git a/nixos/modules/services/networking/inspircd.nix b/nixos/modules/services/networking/inspircd.nix
new file mode 100644
index 00000000000..8cb2b406ee2
--- /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.literalExample "pkgs.inspircd";
+        example = lib.literalExample "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/ircd-hybrid/default.nix b/nixos/modules/services/networking/ircd-hybrid/default.nix
index 0781159b6ee..1f5636e4e3a 100644
--- a/nixos/modules/services/networking/ircd-hybrid/default.nix
+++ b/nixos/modules/services/networking/ircd-hybrid/default.nix
@@ -10,7 +10,7 @@ let
     name = "ircd-hybrid-service";
     scripts = [ "=>/bin" ./control.in ];
     substFiles = [ "=>/conf" ./ircd.conf ];
-    inherit (pkgs) ircdHybrid coreutils su iproute gnugrep procps;
+    inherit (pkgs) ircdHybrid coreutils su iproute2 gnugrep procps;
 
     ipv6Enabled = boolToString config.networking.enableIPv6;
 
diff --git a/nixos/modules/services/networking/iscsi/initiator.nix b/nixos/modules/services/networking/iscsi/initiator.nix
new file mode 100644
index 00000000000..cbc919a2f76
--- /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 = "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..3274878c4fa
--- /dev/null
+++ b/nixos/modules/services/networking/iscsi/root-initiator.nix
@@ -0,0 +1,181 @@
+{ 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;
+    };
+
+    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
+      ''}
+        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/kresd.nix b/nixos/modules/services/networking/kresd.nix
index 4131ff8be5d..9b94c390e98 100644
--- a/nixos/modules/services/networking/kresd.nix
+++ b/nixos/modules/services/networking/kresd.nix
@@ -8,14 +8,14 @@ let
   # Convert systemd-style address specification to kresd config line(s).
   # On Nix level we don't attempt to precisely validate the address specifications.
   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_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 = last al;
-    addrSpec = if al_portOnly == null then "'${head al}'" else "{'::', '127.0.0.1'}";
+    addrSpec = if al_portOnly == null then "'${head al}'" else "{'::', '0.0.0.0'}";
     in # freebind is set for compatibility with earlier kresd services;
        # it could be configurable, for example.
       ''
@@ -29,8 +29,6 @@ let
     + concatMapStrings (mkListen "doh2") cfg.listenDoH
     + cfg.extraConfig
   );
-
-  package = pkgs.knot-resolver;
 in {
   meta.maintainers = [ maintainers.vcunat /* upstream developer */ ];
 
@@ -58,6 +56,15 @@ in {
         and give commands interactively to kresd@1.service.
       '';
     };
+    package = mkOption {
+      type = types.package;
+      description = "
+        knot-resolver package to use.
+      ";
+      default = pkgs.knot-resolver;
+      defaultText = "pkgs.knot-resolver";
+      example = literalExample "pkgs.knot-resolver.override { extraFeatures = true; }";
+    };
     extraConfig = mkOption {
       type = types.lines;
       default = "";
@@ -115,7 +122,7 @@ in {
       };
     users.groups.knot-resolver.gid = null;
 
-    systemd.packages = [ package ]; # the units are patched inside the package a bit
+    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" ];
@@ -123,8 +130,8 @@ in {
         ++ map (i: "kresd@${toString i}.service") (range 1 cfg.instances);
     };
     systemd.services."kresd@".serviceConfig = {
-      ExecStart = "${package}/bin/kresd --noninteractive "
-        + "-c ${package}/lib/knot-resolver/distro-preconfig.lua -c ${configFile}";
+      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";
@@ -137,10 +144,5 @@ in {
     };
     # We don't mind running stop phase from wrong version.  It seems less racy.
     systemd.services."kresd@".stopIfChanged = false;
-
-    # Try cleaning up the previously default location of cache file.
-    # Note that /var/cache/* should always be safe to remove.
-    # TODO: remove later, probably between 20.09 and 21.05
-    systemd.tmpfiles.rules = [ "R /var/cache/kresd" ];
   };
 }
diff --git a/nixos/modules/services/networking/libreswan.nix b/nixos/modules/services/networking/libreswan.nix
index 280158b89f6..81bc4e1cf95 100644
--- a/nixos/modules/services/networking/libreswan.nix
+++ b/nixos/modules/services/networking/libreswan.nix
@@ -85,13 +85,13 @@ in
 
   config = mkIf cfg.enable {
 
-    environment.systemPackages = [ pkgs.libreswan pkgs.iproute ];
+    environment.systemPackages = [ pkgs.libreswan pkgs.iproute2 ];
 
     systemd.services.ipsec = {
       description = "Internet Key Exchange (IKE) Protocol Daemon for IPsec";
       path = [
         "${pkgs.libreswan}"
-        "${pkgs.iproute}"
+        "${pkgs.iproute2}"
         "${pkgs.procps}"
         "${pkgs.nssTools}"
         "${pkgs.iptables}"
@@ -115,8 +115,8 @@ in
         ExecStart = "${libexec}/pluto --config ${configFile} --nofork \$PLUTO_OPTIONS";
         ExecStop = "${libexec}/whack --shutdown";
         ExecStopPost = [
-          "${pkgs.iproute}/bin/ip xfrm policy flush"
-          "${pkgs.iproute}/bin/ip xfrm state flush"
+          "${pkgs.iproute2}/bin/ip xfrm policy flush"
+          "${pkgs.iproute2}/bin/ip xfrm state flush"
           "${ipsec} --stopnflog"
         ];
         ExecReload = "${libexec}/whack --listen";
diff --git a/nixos/modules/services/networking/mosquitto.nix b/nixos/modules/services/networking/mosquitto.nix
index 10b49d9b220..8e814ffd0b9 100644
--- a/nixos/modules/services/networking/mosquitto.nix
+++ b/nixos/modules/services/networking/mosquitto.nix
@@ -20,8 +20,7 @@ let
     acl_file ${aclFile}
     persistence true
     allow_anonymous ${boolToString cfg.allowAnonymous}
-    bind_address ${cfg.host}
-    port ${toString cfg.port}
+    listener ${toString cfg.port} ${cfg.host}
     ${passwordConf}
     ${listenerConf}
     ${cfg.extraConf}
@@ -233,15 +232,50 @@ in
         ExecStart = "${pkgs.mosquitto}/bin/mosquitto -c ${mosquittoConf}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
 
-        ProtectSystem = "strict";
-        ProtectHome = true;
+        # Hardening
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
         PrivateDevices = true;
         PrivateTmp = true;
-        ReadWritePaths = "${cfg.dataDir}";
+        PrivateUsers = true;
+        ProtectClock = true;
         ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
         ProtectKernelModules = true;
         ProtectKernelTunables = true;
-        NoNewPrivileges = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        ProtectSystem = "strict";
+        ReadWritePaths = [
+          cfg.dataDir
+          "/tmp"  # mosquitto_passwd creates files in /tmp before moving them
+        ];
+        ReadOnlyPaths = with cfg.ssl; lib.optionals (enable) [
+          certfile
+          keyfile
+          cafile
+        ];
+        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 = ''
         rm -f ${cfg.dataDir}/passwd
diff --git a/nixos/modules/services/networking/mullvad-vpn.nix b/nixos/modules/services/networking/mullvad-vpn.nix
index 6f595ca4be2..8ce71f26b3e 100644
--- a/nixos/modules/services/networking/mullvad-vpn.nix
+++ b/nixos/modules/services/networking/mullvad-vpn.nix
@@ -28,7 +28,7 @@ with lib;
         "systemd-resolved.service"
       ];
       path = [
-        pkgs.iproute
+        pkgs.iproute2
         # Needed for ping
         "/run/wrappers"
       ];
diff --git a/nixos/modules/services/networking/mxisd.nix b/nixos/modules/services/networking/mxisd.nix
index 482d6ff456b..f29d190c626 100644
--- a/nixos/modules/services/networking/mxisd.nix
+++ b/nixos/modules/services/networking/mxisd.nix
@@ -41,8 +41,8 @@ in {
 
       package = mkOption {
         type = types.package;
-        default = pkgs.mxisd;
-        defaultText = "pkgs.mxisd";
+        default = pkgs.ma1sd;
+        defaultText = "pkgs.ma1sd";
         description = "The mxisd/ma1sd package to use";
       };
 
diff --git a/nixos/modules/services/networking/ncdns.nix b/nixos/modules/services/networking/ncdns.nix
index c1832ad1752..d30fe0f6f6d 100644
--- a/nixos/modules/services/networking/ncdns.nix
+++ b/nixos/modules/services/networking/ncdns.nix
@@ -243,8 +243,10 @@ in
         xlog.journal = true;
     };
 
-    users.users.ncdns =
-      { description = "ncdns daemon user"; };
+    users.users.ncdns = {
+      isSystemUser = true;
+      description = "ncdns daemon user";
+    };
 
     systemd.services.ncdns = {
       description = "ncdns daemon";
diff --git a/nixos/modules/services/networking/nebula.nix b/nixos/modules/services/networking/nebula.nix
new file mode 100644
index 00000000000..e7ebfe1b4db
--- /dev/null
+++ b/nixos/modules/services/networking/nebula.nix
@@ -0,0 +1,219 @@
+{ 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 = "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 = literalExample ''
+                { "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 = literalExample ''
+                {
+                  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
index 2e680544ec2..135f29be58c 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -465,7 +465,7 @@ in {
       restartTriggers = [ configFile overrideNameserversScript ];
 
       # useful binaries for user-specified hooks
-      path = [ pkgs.iproute pkgs.util-linux pkgs.coreutils ];
+      path = [ pkgs.iproute2 pkgs.util-linux pkgs.coreutils ];
       aliases = [ "dbus-org.freedesktop.nm-dispatcher.service" ];
     };
 
@@ -484,6 +484,8 @@ in {
       })
     ];
 
+    boot.kernelModules = [ "ctr" ];
+
     security.polkit.extraConfig = polkitConf;
 
     services.dbus.packages = cfg.packages
diff --git a/nixos/modules/services/networking/nomad.nix b/nixos/modules/services/networking/nomad.nix
index 9f1b443b89b..48689f1195c 100644
--- a/nixos/modules/services/networking/nomad.nix
+++ b/nixos/modules/services/networking/nomad.nix
@@ -119,7 +119,7 @@ in
       path = cfg.extraPackages ++ (with pkgs; [
         # Client mode requires at least the following:
         coreutils
-        iproute
+        iproute2
         iptables
       ]);
 
diff --git a/nixos/modules/services/networking/openvpn.nix b/nixos/modules/services/networking/openvpn.nix
index 650f9c84ac7..b4c2c944b6e 100644
--- a/nixos/modules/services/networking/openvpn.nix
+++ b/nixos/modules/services/networking/openvpn.nix
@@ -63,7 +63,7 @@ let
       wantedBy = optional cfg.autoStart "multi-user.target";
       after = [ "network.target" ];
 
-      path = [ pkgs.iptables pkgs.iproute pkgs.nettools ];
+      path = [ pkgs.iptables pkgs.iproute2 pkgs.nettools ];
 
       serviceConfig.ExecStart = "@${openvpn}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
       serviceConfig.Restart = "always";
diff --git a/nixos/modules/services/networking/pixiecore.nix b/nixos/modules/services/networking/pixiecore.nix
index 85aa40784af..d2642c82c2d 100644
--- a/nixos/modules/services/networking/pixiecore.nix
+++ b/nixos/modules/services/networking/pixiecore.nix
@@ -93,6 +93,7 @@ in
     users.users.pixiecore = {
       description = "Pixiecore daemon user";
       group = "pixiecore";
+      isSystemUser = true;
     };
 
     networking.firewall = mkIf cfg.openFirewall {
diff --git a/nixos/modules/services/networking/pleroma.nix b/nixos/modules/services/networking/pleroma.nix
index 9b2bf9f6124..2687230a158 100644
--- a/nixos/modules/services/networking/pleroma.nix
+++ b/nixos/modules/services/networking/pleroma.nix
@@ -75,6 +75,7 @@ in {
         description = "Pleroma user";
         home = cfg.stateDir;
         extraGroups = [ cfg.group ];
+        isSystemUser = true;
       };
       groups."${cfg.group}" = {};
     };
diff --git a/nixos/modules/services/networking/privoxy.nix b/nixos/modules/services/networking/privoxy.nix
index 7caae328203..df818baa465 100644
--- a/nixos/modules/services/networking/privoxy.nix
+++ b/nixos/modules/services/networking/privoxy.nix
@@ -4,26 +4,46 @@ with lib;
 
 let
 
-  inherit (pkgs) privoxy;
-
   cfg = config.services.privoxy;
 
-  confFile = pkgs.writeText "privoxy.conf" (''
-    user-manual ${privoxy}/share/doc/privoxy/user-manual
-    confdir ${privoxy}/etc/
-    listen-address  ${cfg.listenAddress}
-    enable-edit-actions ${if (cfg.enableEditActions == true) then "1" else "0"}
-    ${concatMapStrings (f: "actionsfile ${f}\n") cfg.actionsFiles}
-    ${concatMapStrings (f: "filterfile ${f}\n") cfg.filterFiles}
-  '' + optionalString cfg.enableTor ''
-    forward-socks5t / 127.0.0.1:9063 .
-    toggle 1
-    enable-remote-toggle 0
-    enable-edit-actions 0
-    enable-remote-http-toggle 0
-  '' + ''
-    ${cfg.extraConfig}
-  '');
+  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
 
@@ -31,70 +51,144 @@ in
 
   ###### interface
 
-  options = {
+  options.services.privoxy = {
 
-    services.privoxy = {
+    enable = mkEnableOption "Privoxy, non-caching filtering proxy";
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to enable the 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.
+      '';
+    };
 
-      listenAddress = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8118";
-        description = ''
-          Address the proxy server is listening to.
-        '';
-      };
+    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.
 
-      actionsFiles = mkOption {
-        type = types.listOf types.str;
-        example = [ "match-all.action" "default.action" "/etc/privoxy/user.action" ];
-        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.
-        '';
-      };
+        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.
 
-      filterFiles = mkOption {
-        type = types.listOf types.str;
-        example = [ "default.filter" "/etc/privoxy/user.filter" ];
-        default = [ "default.filter" ];
-        description = ''
-          List of paths to Privoxy filter files.
-          These paths may either be absolute or relative to the privoxy configuration directory.
-        '';
-      };
+        <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>
+      '';
+    };
 
-      enableEditActions = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether or not the web-based actions file editor may be used.
-        '';
-      };
+    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.
 
-      enableTor = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to configure Privoxy to use Tor's faster SOCKS port,
-          suitable for HTTP.
-        '';
-      };
+        Depending on the traffic, you may want to reduce the lifetime to limit
+        the disk usage, since Privoxy itself never deletes the certificates.
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "" ;
-        description = ''
-          Extra configuration. Contents will be added verbatim to the configuration file.
-        '';
+        <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 = literalExample ''
+        { # 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>
+      '';
     };
 
   };
@@ -104,23 +198,33 @@ in
   config = mkIf cfg.enable {
 
     users.users.privoxy = {
+      description = "Privoxy daemon user";
       isSystemUser = true;
-      home = "/var/empty";
       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.ExecStart = "${privoxy}/bin/privoxy --no-daemon --user privoxy ${confFile}";
-
-      serviceConfig.PrivateDevices = true;
-      serviceConfig.PrivateTmp = true;
-      serviceConfig.ProtectHome = true;
-      serviceConfig.ProtectSystem = "full";
+      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 [
@@ -128,8 +232,48 @@ in
       { 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/quagga.nix b/nixos/modules/services/networking/quagga.nix
deleted file mode 100644
index 5acdd5af8f8..00000000000
--- a/nixos/modules/services/networking/quagga.nix
+++ /dev/null
@@ -1,185 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.quagga;
-
-  services = [ "babel" "bgp" "isis" "ospf6" "ospf" "pim" "rip" "ripng" ];
-  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"
-        ''
-          ! Quagga ${daemonName service} configuration
-          !
-          hostname ${config.networking.hostName}
-          log syslog
-          service password-encryption
-          !
-          ${scfg.config}
-          !
-          end
-        '';
-
-  serviceOptions = service:
-    {
-      enable = mkEnableOption "the Quagga ${toUpper service} routing protocol";
-
-      configFile = mkOption {
-        type = types.nullOr types.path;
-        default = null;
-        example = "/etc/quagga/${daemonName service}.conf";
-        description = ''
-          Configuration file to use for Quagga ${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 = "127.0.0.1";
-        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.quagga = {
-        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.quagga = (genAttrs services serviceOptions); }
-  ];
-
-  ###### implementation
-
-  config = mkIf (any isEnabled allServices) {
-
-    environment.systemPackages = [
-      pkgs.quagga               # for the vtysh tool
-    ];
-
-    users.users.quagga = {
-      description = "Quagga daemon user";
-      isSystemUser = true;
-      group = "quagga";
-    };
-
-    users.groups = {
-      quagga = {};
-      # Members of the quaggavty group can use vtysh to inspect the Quagga daemons
-      quaggavty = { members = [ "quagga" ]; };
-    };
-
-    systemd.services =
-      let
-        quaggaService = service:
-          let
-            scfg = cfg.${service};
-            daemon = daemonName service;
-          in
-            nameValuePair daemon ({
-              wantedBy = [ "multi-user.target" ];
-              restartTriggers = [ (configFile service) ];
-
-              serviceConfig = {
-                Type = "forking";
-                PIDFile = "/run/quagga/${daemon}.pid";
-                ExecStart = "@${pkgs.quagga}/libexec/quagga/${daemon} ${daemon} -d -f ${configFile service}"
-                  + optionalString (scfg.vtyListenAddress != "") " -A ${scfg.vtyListenAddress}"
-                  + optionalString (scfg.vtyListenPort != null) " -P ${toString scfg.vtyListenPort}";
-                ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-                Restart = "on-abort";
-              };
-            } // (
-              if service == "zebra" then
-                {
-                  description = "Quagga Zebra routing manager";
-                  unitConfig.Documentation = "man:zebra(8)";
-                  after = [ "network.target" ];
-                  preStart = ''
-                    install -m 0755 -o quagga -g quagga -d /run/quagga
-
-                    ${pkgs.iproute}/bin/ip route flush proto zebra
-                  '';
-                }
-              else
-                {
-                  description = "Quagga ${toUpper service} routing daemon";
-                  unitConfig.Documentation = "man:${daemon}(8) man:zebra(8)";
-                  bindsTo = [ "zebra.service" ];
-                  after = [ "network.target" "zebra.service" ];
-                }
-            ));
-       in
-         listToAttrs (map quaggaService (filter isEnabled allServices));
-
-  };
-
-  meta.maintainers = with lib.maintainers; [ tavyc ];
-
-}
diff --git a/nixos/modules/services/networking/rxe.nix b/nixos/modules/services/networking/rxe.nix
index c7d174a00de..868e2c81ccb 100644
--- a/nixos/modules/services/networking/rxe.nix
+++ b/nixos/modules/services/networking/rxe.nix
@@ -39,11 +39,11 @@ in {
         Type = "oneshot";
         RemainAfterExit = true;
         ExecStart = map ( x:
-          "${pkgs.iproute}/bin/rdma link add rxe_${x} type rxe netdev ${x}"
+          "${pkgs.iproute2}/bin/rdma link add rxe_${x} type rxe netdev ${x}"
           ) cfg.interfaces;
 
         ExecStop = map ( x:
-          "${pkgs.iproute}/bin/rdma link delete rxe_${x}"
+          "${pkgs.iproute2}/bin/rdma link delete rxe_${x}"
           ) cfg.interfaces;
       };
     };
diff --git a/nixos/modules/services/networking/searx.nix b/nixos/modules/services/networking/searx.nix
index a515e4a3dc3..04f7d7e31f4 100644
--- a/nixos/modules/services/networking/searx.nix
+++ b/nixos/modules/services/networking/searx.nix
@@ -4,23 +4,25 @@ 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
-    cat <<'EOF' > settings.yml
-      ${builtins.toJSON cfg.settings}
-    EOF
+    (
+      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
-
-    # set strict permissions
-    chmod 400 settings.yml
   '';
 
   settingType = with types; (oneOf
diff --git a/nixos/modules/services/networking/spacecookie.nix b/nixos/modules/services/networking/spacecookie.nix
index c4d06df6ad4..e0bef9e9628 100644
--- a/nixos/modules/services/networking/spacecookie.nix
+++ b/nixos/modules/services/networking/spacecookie.nix
@@ -4,10 +4,22 @@ with lib;
 
 let
   cfg = config.services.spacecookie;
-  configFile = pkgs.writeText "spacecookie.json" (lib.generators.toJSON {} {
-    inherit (cfg) hostname port root;
-  });
+
+  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 = {
 
@@ -15,32 +27,149 @@ in {
 
       enable = mkEnableOption "spacecookie";
 
-      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.";
+      package = mkOption {
+        type = types.package;
+        default = pkgs.spacecookie;
+        defaultText = literalExample "pkgs.spacecookie";
+        example = literalExample "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.";
+        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>.
+        '';
       };
 
-      root = mkOption {
-        type = types.path;
-        default = "/srv/gopher";
-        description = "The root directory spacecookie serves via gopher.";
+      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 = [ "[::]:${toString cfg.port}" ];
+      listenStreams = [ "${cfg.address}:${toString cfg.port}" ];
       socketConfig = {
         BindIPv6Only = "both";
       };
@@ -53,7 +182,7 @@ in {
 
       serviceConfig = {
         Type = "notify";
-        ExecStart = "${pkgs.haskellPackages.spacecookie}/bin/spacecookie ${configFile}";
+        ExecStart = "${lib.getBin cfg.package}/bin/spacecookie ${configFile}";
         FileDescriptorStoreMax = 1;
 
         DynamicUser = true;
@@ -79,5 +208,9 @@ in {
         RestrictAddressFamilies = "AF_UNIX AF_INET6";
       };
     };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
   };
 }
diff --git a/nixos/modules/services/networking/sslh.nix b/nixos/modules/services/networking/sslh.nix
index 4c2740d2019..abe96f60f81 100644
--- a/nixos/modules/services/networking/sslh.nix
+++ b/nixos/modules/services/networking/sslh.nix
@@ -132,7 +132,7 @@ in
           { table = "mangle"; command = "OUTPUT ! -o lo -p tcp -m connmark --mark 0x02/0x0f -j CONNMARK --restore-mark --mask 0x0f"; }
         ];
       in {
-        path = [ pkgs.iptables pkgs.iproute pkgs.procps ];
+        path = [ pkgs.iptables pkgs.iproute2 pkgs.procps ];
 
         preStart = ''
           # Cleanup old iptables entries which might be still there
diff --git a/nixos/modules/services/networking/strongswan-swanctl/module.nix b/nixos/modules/services/networking/strongswan-swanctl/module.nix
index f67eedac296..6e619f22546 100644
--- a/nixos/modules/services/networking/strongswan-swanctl/module.nix
+++ b/nixos/modules/services/networking/strongswan-swanctl/module.nix
@@ -63,7 +63,7 @@ in  {
       description = "strongSwan IPsec IKEv1/IKEv2 daemon using swanctl";
       wantedBy = [ "multi-user.target" ];
       after    = [ "network-online.target" ];
-      path     = with pkgs; [ kmod iproute iptables util-linux ];
+      path     = with pkgs; [ kmod iproute2 iptables util-linux ];
       environment = {
         STRONGSWAN_CONF = pkgs.writeTextFile {
           name = "strongswan.conf";
diff --git a/nixos/modules/services/networking/strongswan.nix b/nixos/modules/services/networking/strongswan.nix
index f6170b81365..401f7be4028 100644
--- a/nixos/modules/services/networking/strongswan.nix
+++ b/nixos/modules/services/networking/strongswan.nix
@@ -152,7 +152,7 @@ in
     systemd.services.strongswan = {
       description = "strongSwan IPSec Service";
       wantedBy = [ "multi-user.target" ];
-      path = with pkgs; [ kmod iproute iptables util-linux ]; # XXX Linux
+      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; };
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
index 1a1474595be..9a28a266a92 100644
--- a/nixos/modules/services/networking/tailscale.nix
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -28,6 +28,7 @@ in {
     systemd.packages = [ cfg.package ];
     systemd.services.tailscaled = {
       wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.openresolv ];
       serviceConfig.Environment = "PORT=${toString cfg.port}";
     };
   };
diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix
index 622c3d8ea43..a8747e244a9 100644
--- a/nixos/modules/services/networking/unbound.nix
+++ b/nixos/modules/services/networking/unbound.nix
@@ -4,51 +4,28 @@ with lib;
 let
   cfg = config.services.unbound;
 
-  stateDir = "/var/lib/unbound";
-
-  access = concatMapStringsSep "\n  " (x: "access-control: ${x} allow") cfg.allowedAccess;
-
-  interfaces = concatMapStringsSep "\n  " (x: "interface: ${x}") cfg.interfaces;
-
-  isLocalAddress = x: substring 0 3 x == "::1" || substring 0 9 x == "127.0.0.1";
-
-  forward =
-    optionalString (any isLocalAddress cfg.forwardAddresses) ''
-      do-not-query-localhost: no
-    ''
-    + optionalString (cfg.forwardAddresses != []) ''
-      forward-zone:
-        name: .
-    ''
-    + concatMapStringsSep "\n" (x: "    forward-addr: ${x}") cfg.forwardAddresses;
-
-  rootTrustAnchorFile = "${stateDir}/root.key";
-
-  trustAnchor = optionalString cfg.enableRootTrustAnchor
-    "auto-trust-anchor-file: ${rootTrustAnchorFile}";
-
-  confFile = pkgs.writeText "unbound.conf" ''
-    server:
-      ip-freebind: yes
-      directory: "${stateDir}"
-      username: unbound
-      chroot: ""
-      pidfile: ""
-      # when running under systemd there is no need to daemonize
-      do-daemonize: no
-      ${interfaces}
-      ${access}
-      ${trustAnchor}
-    ${lib.optionalString (cfg.localControlSocketPath != null) ''
-      remote-control:
-        control-enable: yes
-        control-interface: ${cfg.localControlSocketPath}
-    ''}
-    ${cfg.extraConfig}
-    ${forward}
-  '';
-in
-{
+  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");
+
+  confFile = pkgs.writeText "unbound.conf" (concatStringsSep "\n" ((mapAttrsToList (toConf "") cfg.settings) ++ [""]));
+
+  rootTrustAnchorFile = "${cfg.stateDir}/root.key";
+
+in {
 
   ###### interface
 
@@ -64,25 +41,30 @@ in
         description = "The unbound package to use";
       };
 
-      allowedAccess = mkOption {
-        default = [ "127.0.0.0/24" ];
-        type = types.listOf types.str;
-        description = "What networks are allowed to use unbound as a resolver.";
+      user = mkOption {
+        type = types.str;
+        default = "unbound";
+        description = "User account under which unbound runs.";
       };
 
-      interfaces = mkOption {
-        default = [ "127.0.0.1" ] ++ optional config.networking.enableIPv6 "::1";
-        type = types.listOf types.str;
-        description =  ''
-          What addresses the server should listen on. This supports the interface syntax documented in
-          <citerefentry><refentrytitle>unbound.conf</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
-        '';
+      group = mkOption {
+        type = types.str;
+        default = "unbound";
+        description = "Group under which unbound runs.";
       };
 
-      forwardAddresses = mkOption {
-        default = [];
-        type = types.listOf types.str;
-        description = "What servers to forward queries to.";
+      stateDir = mkOption {
+        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 {
@@ -106,23 +88,66 @@ in
           and group will be <literal>nogroup</literal>.
 
           Users that should be permitted to access the socket must be in the
-          <literal>unbound</literal> group.
+          <literal>config.services.unbound.group</literal> group.
 
           If this option is <literal>null</literal> remote control will not be
-          configured at all. Unbounds default values apply.
+          enabled. Unbounds default values apply.
         '';
       };
 
-      extraConfig = mkOption {
-        default = "";
-        type = types.lines;
+      settings = mkOption {
+        default = {};
+        type = with types; submodule {
+
+          freeformType = let
+            validSettingsPrimitiveTypes = oneOf [ int str bool float ];
+            validSettingsTypes = oneOf [ validSettingsPrimitiveTypes (listOf validSettingsPrimitiveTypes) ];
+            settingsType = (attrsOf validSettingsTypes);
+          in attrsOf (oneOf [ string 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 = literalExample ''
+          {
+            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 = ''
-          Extra unbound config. See
-          <citerefentry><refentrytitle>unbound.conf</refentrytitle><manvolnum>8
-          </manvolnum></citerefentry>.
+          Declarative Unbound configuration
+          See the <citerefentry><refentrytitle>unbound.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> manpage for a list of
+          available options.
         '';
       };
-
     };
   };
 
@@ -130,23 +155,56 @@ in
 
   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;
+      };
+      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.unbound = {
-      description = "unbound daemon user";
-      isSystemUser = true;
-      group = lib.mkIf (cfg.localControlSocketPath != null) (lib.mkDefault "unbound");
+    users.users = mkIf (cfg.user == "unbound") {
+      unbound = {
+        description = "unbound daemon user";
+        isSystemUser = true;
+        group = cfg.group;
+      };
     };
 
-    # We need a group so that we can give users access to the configured
-    # control socket. Unbound allows access to the socket only to the unbound
-    # user and the primary group.
-    users.groups = lib.mkIf (cfg.localControlSocketPath != null) {
+    users.groups = mkIf (cfg.group == "unbound") {
       unbound = {};
     };
 
-    networking.resolvconf.useLocalResolver = mkDefault true;
+    networking = mkIf cfg.resolveLocalQueries {
+      resolvconf = {
+        useLocalResolver = mkDefault true;
+      };
 
+      networkmanager.dns = "unbound";
+    };
 
     environment.etc."unbound/unbound.conf".source = confFile;
 
@@ -156,8 +214,15 @@ in
       before = [ "nss-lookup.target" ];
       wantedBy = [ "multi-user.target" "nss-lookup.target" ];
 
-      preStart = lib.mkIf cfg.enableRootTrustAnchor ''
-        ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
+      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 = [
@@ -181,8 +246,8 @@ in
           "CAP_SYS_RESOURCE"
         ];
 
-        User = "unbound";
-        Group = lib.mkIf (cfg.localControlSocketPath != null) (lib.mkDefault "unbound");
+        User = cfg.user;
+        Group = cfg.group;
 
         MemoryDenyWriteExecute = true;
         NoNewPrivileges = true;
@@ -211,9 +276,29 @@ in
         RestrictNamespaces = true;
         LockPersonality = true;
         RestrictSUIDSGID = true;
+
+        Restart = "on-failure";
+        RestartSec = "5s";
       };
     };
-    # If networkmanager is enabled, ask it to interface with unbound.
-    networking.networkmanager.dns = "unbound";
   };
+
+  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/wg-quick.nix b/nixos/modules/services/networking/wg-quick.nix
index 02fe40a22a1..3b76de58548 100644
--- a/nixos/modules/services/networking/wg-quick.nix
+++ b/nixos/modules/services/networking/wg-quick.nix
@@ -57,7 +57,7 @@ let
 
       preUp = mkOption {
         example = literalExample ''
-          ${pkgs.iproute}/bin/ip netns add foo
+          ${pkgs.iproute2}/bin/ip netns add foo
         '';
         default = "";
         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
@@ -68,7 +68,7 @@ let
 
       preDown = mkOption {
         example = literalExample ''
-          ${pkgs.iproute}/bin/ip netns del foo
+          ${pkgs.iproute2}/bin/ip netns del foo
         '';
         default = "";
         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
@@ -79,7 +79,7 @@ let
 
       postUp = mkOption {
         example = literalExample ''
-          ${pkgs.iproute}/bin/ip netns add foo
+          ${pkgs.iproute2}/bin/ip netns add foo
         '';
         default = "";
         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
@@ -90,7 +90,7 @@ let
 
       postDown = mkOption {
         example = literalExample ''
-          ${pkgs.iproute}/bin/ip netns del foo
+          ${pkgs.iproute2}/bin/ip netns del foo
         '';
         default = "";
         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index e07020349cf..043bce16e54 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -63,7 +63,7 @@ let
 
       preSetup = mkOption {
         example = literalExample ''
-          ${pkgs.iproute}/bin/ip netns add foo
+          ${pkgs.iproute2}/bin/ip netns add foo
         '';
         default = "";
         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
@@ -238,7 +238,7 @@ let
         wantedBy = [ "wireguard-${name}.service" ];
         requiredBy = [ "wireguard-${name}.service" ];
         before = [ "wireguard-${name}.service" ];
-        path = with pkgs; [ wireguard ];
+        path = with pkgs; [ wireguard-tools ];
 
         serviceConfig = {
           Type = "oneshot";
@@ -246,12 +246,15 @@ let
         };
 
         script = ''
-          mkdir --mode 0644 -p "${dirOf values.privateKeyFile}"
+          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
-            touch "${values.privateKeyFile}"
-            chmod 0600 "${values.privateKeyFile}"
-            wg genkey > "${values.privateKeyFile}"
-            chmod 0400 "${values.privateKeyFile}"
+            # Write private key file with atomically-correct permissions.
+            (set -e; umask 077; wg genkey > "${values.privateKeyFile}")
           fi
         '';
       };
@@ -278,7 +281,7 @@ let
         wantedBy = [ "multi-user.target" "wireguard-${interfaceName}.service" ];
         environment.DEVICE = interfaceName;
         environment.WG_ENDPOINT_RESOLUTION_RETRIES = "infinity";
-        path = with pkgs; [ iproute wireguard-tools ];
+        path = with pkgs; [ iproute2 wireguard-tools ];
 
         serviceConfig = {
           Type = "oneshot";
@@ -333,7 +336,7 @@ let
         after = [ "network.target" "network-online.target" ];
         wantedBy = [ "multi-user.target" ];
         environment.DEVICE = name;
-        path = with pkgs; [ kmod iproute wireguard-tools ];
+        path = with pkgs; [ kmod iproute2 wireguard-tools ];
 
         serviceConfig = {
           Type = "oneshot";
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index 61482596763..8a0685c3d96 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -3,6 +3,10 @@
 with lib;
 
 let
+  package = if cfg.allowAuxiliaryImperativeNetworks
+    then pkgs.wpa_supplicant_ro_ssids
+    else pkgs.wpa_supplicant;
+
   cfg = config.networking.wireless;
   configFile = if cfg.networks != {} || cfg.extraConfig != "" || cfg.userControlled.enable then pkgs.writeText "wpa_supplicant.conf" ''
     ${optionalString cfg.userControlled.enable ''
@@ -47,6 +51,16 @@ in {
         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>.
+        '';
+      };
+
       networks = mkOption {
         type = types.attrsOf (types.submodule {
           options = {
@@ -211,9 +225,9 @@ in {
       message = ''options networking.wireless."${name}".{psk,pskRaw,auth} are mutually exclusive'';
     });
 
-    environment.systemPackages =  [ pkgs.wpa_supplicant ];
+    environment.systemPackages = [ package ];
 
-    services.dbus.packages = [ pkgs.wpa_supplicant ];
+    services.dbus.packages = [ package ];
     services.udev.packages = [ pkgs.crda ];
 
     # FIXME: start a separate wpa_supplicant instance per interface.
@@ -230,13 +244,17 @@ in {
       wantedBy = [ "multi-user.target" ];
       stopIfChanged = false;
 
-      path = [ pkgs.wpa_supplicant ];
+      path = [ package ];
 
-      script = ''
+      script = let
+        configStr = if cfg.allowAuxiliaryImperativeNetworks
+          then "-c /etc/wpa_supplicant.conf -I ${configFile}"
+          else "-c ${configFile}";
+      in ''
         if [ -f /etc/wpa_supplicant.conf -a "/etc/wpa_supplicant.conf" != "${configFile}" ]
         then echo >&2 "<3>/etc/wpa_supplicant.conf present but ignored. Generated ${configFile} is used instead."
         fi
-        iface_args="-s -u -D${cfg.driver} -c ${configFile}"
+        iface_args="-s -u -D${cfg.driver} ${configStr}"
         ${if ifaces == [] then ''
           for i in $(cd /sys/class/net && echo *); do
             DEVTYPE=
diff --git a/nixos/modules/services/networking/zerobin.nix b/nixos/modules/services/networking/zerobin.nix
index 78de246a816..16db25d6230 100644
--- a/nixos/modules/services/networking/zerobin.nix
+++ b/nixos/modules/services/networking/zerobin.nix
@@ -88,7 +88,7 @@ in
         enable = true;
         after = [ "network.target" ];
         wantedBy = [ "multi-user.target" ];
-        serviceConfig.ExecStart = "${pkgs.pythonPackages.zerobin}/bin/zerobin ${cfg.listenAddress} ${toString cfg.listenPort} false ${cfg.user} ${cfg.group} ${zerobin_config}";
+        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;
diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix
index b19cd073252..d2b36d9e754 100644
--- a/nixos/modules/services/printing/cupsd.nix
+++ b/nixos/modules/services/printing/cupsd.nix
@@ -104,7 +104,7 @@ let
     ignoreCollisions = true;
   };
 
-  filterGutenprint = pkgs: filter (pkg: pkg.meta.isGutenprint or false == true) pkgs;
+  filterGutenprint = filter (pkg: pkg.meta.isGutenprint or false == true);
   containsGutenprint = pkgs: length (filterGutenprint pkgs) > 0;
   getGutenprint = pkgs: head (filterGutenprint pkgs);
 
diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix
index cf0d72d5c53..0c24972823d 100644
--- a/nixos/modules/services/security/fail2ban.nix
+++ b/nixos/modules/services/security/fail2ban.nix
@@ -62,6 +62,22 @@ in
         description = "The firewall package used by fail2ban service.";
       };
 
+      extraPackages = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        example = lib.literalExample "[ 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;
@@ -243,7 +259,7 @@ in
       restartTriggers = [ fail2banConf jailConf pathsConf ];
       reloadIfChanged = true;
 
-      path = [ cfg.package cfg.packageFirewall pkgs.iproute ];
+      path = [ cfg.package cfg.packageFirewall pkgs.iproute2 ] ++ cfg.extraPackages;
 
       unitConfig.Documentation = "man:fail2ban(1)";
 
@@ -291,7 +307,7 @@ in
       ''}
       # Miscellaneous options
       ignoreip    = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}
-      maxretry    = 3
+      maxretry    = ${toString cfg.maxretry}
       backend     = systemd
       # Actions
       banaction   = ${cfg.banaction}
diff --git a/nixos/modules/services/security/fprintd.nix b/nixos/modules/services/security/fprintd.nix
index 48f8a9616c3..fe0fba5b45d 100644
--- a/nixos/modules/services/security/fprintd.nix
+++ b/nixos/modules/services/security/fprintd.nix
@@ -5,6 +5,7 @@ with lib;
 let
 
   cfg = config.services.fprintd;
+  fprintdPkg = if cfg.tod.enable then pkgs.fprintd-tod else pkgs.fprintd;
 
 in
 
@@ -17,25 +18,30 @@ in
 
     services.fprintd = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to enable fprintd daemon and PAM module for fingerprint readers handling.
-        '';
-      };
+      enable = mkEnableOption "fprintd daemon and PAM module for fingerprint readers handling";
 
       package = mkOption {
         type = types.package;
-        default = pkgs.fprintd;
-        defaultText = "pkgs.fprintd";
+        default = fprintdPkg;
+        defaultText = "if cfg.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 = literalExample "pkgs.libfprint-2-tod1-goodix";
+          description = ''
+            Touch OEM Drivers (TOD) package to use.
+          '';
+        };
+      };
+    };
   };
 
 
@@ -49,6 +55,10 @@ in
 
     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/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix
index 77c579279ab..e85fd4b75df 100644
--- a/nixos/modules/services/security/oauth2_proxy.nix
+++ b/nixos/modules/services/security/oauth2_proxy.nix
@@ -90,10 +90,10 @@ in
 
     package = mkOption {
       type = types.package;
-      default = pkgs.oauth2_proxy;
-      defaultText = "pkgs.oauth2_proxy";
+      default = pkgs.oauth2-proxy;
+      defaultText = "pkgs.oauth2-proxy";
       description = ''
-        The package that provides oauth2_proxy.
+        The package that provides oauth2-proxy.
       '';
     };
 
diff --git a/nixos/modules/services/security/oauth2_proxy_nginx.nix b/nixos/modules/services/security/oauth2_proxy_nginx.nix
index 553638ad496..d82ddb894ea 100644
--- a/nixos/modules/services/security/oauth2_proxy_nginx.nix
+++ b/nixos/modules/services/security/oauth2_proxy_nginx.nix
@@ -23,7 +23,8 @@ in
   config.services.oauth2_proxy = mkIf (cfg.virtualHosts != [] && (hasPrefix "127.0.0.1:" cfg.proxy)) {
     enable = true;
   };
-  config.services.nginx = mkMerge ((optional (cfg.virtualHosts != []) {
+  config.services.nginx = mkIf config.services.oauth2_proxy.enable (mkMerge
+  ((optional (cfg.virtualHosts != []) {
     recommendedProxySettings = true; # needed because duplicate headers
   }) ++ (map (vhost: {
     virtualHosts.${vhost} = {
@@ -60,5 +61,5 @@ in
       '';
 
     };
-  }) cfg.virtualHosts));
+  }) cfg.virtualHosts)));
 }
diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix
index c2988858e56..2696dca4c76 100644
--- a/nixos/modules/services/security/privacyidea.nix
+++ b/nixos/modules/services/security/privacyidea.nix
@@ -57,6 +57,26 @@ in
     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";
@@ -206,7 +226,7 @@ in
         wantedBy = [ "multi-user.target" ];
         after = [ "postgresql.service" ];
         path = with pkgs; [ openssl ];
-        environment.PRIVACYIDEA_CONFIGFILE = piCfgFile;
+        environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
         preStart = let
           pi-manage = "${pkgs.sudo}/bin/sudo -u privacyidea -HE ${penv}/bin/pi-manage";
           pgsu = config.services.postgresql.superUser;
@@ -214,6 +234,10 @@ in
         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
             ${pkgs.sudo}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user}
             ${pkgs.sudo}/bin/sudo -u ${pgsu} ${psql}/bin/createdb --owner ${cfg.user} privacyidea
@@ -231,6 +255,7 @@ in
           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";
@@ -239,6 +264,7 @@ in
 
       users.users.privacyidea = mkIf (cfg.user == "privacyidea") {
         group = cfg.group;
+        isSystemUser = true;
       };
 
       users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {};
@@ -269,6 +295,7 @@ in
 
       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/sshguard.nix b/nixos/modules/services/security/sshguard.nix
index 72de11a9254..53bd9efa5ac 100644
--- a/nixos/modules/services/security/sshguard.nix
+++ b/nixos/modules/services/security/sshguard.nix
@@ -5,6 +5,21 @@ 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
@@ -85,20 +100,7 @@ in {
 
   config = mkIf cfg.enable {
 
-    environment.etc."sshguard.conf".text = 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 ''
-      BACKEND="${pkgs.sshguard}/libexec/${backend}"
-      LOGREADER="LANG=C ${pkgs.systemd}/bin/journalctl ${args}"
-    '';
+    environment.etc."sshguard.conf".source = configFile;
 
     systemd.services.sshguard = {
       description = "SSHGuard brute-force attacks protection system";
@@ -107,9 +109,11 @@ in {
       after = [ "network.target" ];
       partOf = optional config.networking.firewall.enable "firewall.service";
 
+      restartTriggers = [ configFile ];
+
       path = with pkgs; if config.networking.nftables.enable
-        then [ nftables iproute systemd ]
-        else [ iptables ipset iproute systemd ];
+        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
diff --git a/nixos/modules/services/security/step-ca.nix b/nixos/modules/services/security/step-ca.nix
new file mode 100644
index 00000000000..64eee11f588
--- /dev/null
+++ b/nixos/modules/services/security/step-ca.nix
@@ -0,0 +1,134 @@
+{ 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;
+        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 = {
+          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 upstream yet
+          # PrivateUsers = true; # doesn't work with privileged ports therefore not supported by upstream
+
+          DynamicUser = true;
+          StateDirectory = "step-ca";
+        };
+      };
+
+      networking.firewall = lib.mkIf cfg.openFirewall {
+        allowedTCPPorts = [ cfg.port ];
+      };
+    }
+  );
+}
diff --git a/nixos/modules/services/system/cloud-init.nix b/nixos/modules/services/system/cloud-init.nix
index f83db30c1f0..eb82b738e49 100644
--- a/nixos/modules/services/system/cloud-init.nix
+++ b/nixos/modules/services/system/cloud-init.nix
@@ -5,7 +5,7 @@ with lib;
 let cfg = config.services.cloud-init;
     path = with pkgs; [
       cloud-init
-      iproute
+      iproute2
       nettools
       openssh
       shadow
diff --git a/nixos/modules/services/system/localtime.nix b/nixos/modules/services/system/localtime.nix
index 8f8e2e2e933..bb99e5e36ff 100644
--- a/nixos/modules/services/system/localtime.nix
+++ b/nixos/modules/services/system/localtime.nix
@@ -29,15 +29,14 @@ in {
       };
     };
 
-    # We use the 'out' output, since localtime has its 'bin' output
-    # first, so that is what we get if we use the derivation bare.
     # Install the polkit rules.
-    environment.systemPackages = [ pkgs.localtime.out ];
+    environment.systemPackages = [ pkgs.localtime ];
     # Install the systemd unit.
-    systemd.packages = [ pkgs.localtime.out ];
+    systemd.packages = [ pkgs.localtime ];
 
     users.users.localtimed = {
-      description = "Taskserver user";
+      description = "localtime daemon";
+      isSystemUser = true;
     };
 
     systemd.services.localtime = {
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index 7bec073e26f..1dec111b829 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -402,6 +402,7 @@ in
           mr ${getLib pkgs.util-linuxMinimal.out}/lib/libuuid.so*,
           mr ${getLib pkgs.xz}/lib/liblzma*.so*,
           mr ${getLib pkgs.zlib}/lib/libz*.so*,
+          mr ${getLib pkgs.brotli}/lib/libbrotli*.so*,
 
           r @{PROC}/sys/kernel/random/uuid,
           r @{PROC}/sys/vm/overcommit_memory,
diff --git a/nixos/modules/services/ttys/getty.nix b/nixos/modules/services/ttys/getty.nix
index ecfabef5fb1..2480e681de8 100644
--- a/nixos/modules/services/ttys/getty.nix
+++ b/nixos/modules/services/ttys/getty.nix
@@ -5,17 +5,16 @@ with lib;
 let
   cfg = config.services.getty;
 
-  loginArgs = [
+  baseArgs = [
     "--login-program" "${pkgs.shadow}/bin/login"
   ] ++ optionals (cfg.autologinUser != null) [
     "--autologin" cfg.autologinUser
   ] ++ optionals (cfg.loginOptions != null) [
     "--login-options" cfg.loginOptions
-  ];
+  ] ++ cfg.extraArgs;
 
-  gettyCmd = extraArgs:
-    "@${pkgs.util-linux}/sbin/agetty agetty ${escapeShellArgs loginArgs} "
-      + extraArgs;
+  gettyCmd = args:
+    "@${pkgs.util-linux}/sbin/agetty agetty ${escapeShellArgs baseArgs} ${args}";
 
 in
 
@@ -54,7 +53,16 @@ in
           will not be invoked with a <option>--login-options</option>
           option.
         '';
-        example = "-h darkstar -- \u";
+        example = "-h darkstar -- \\u";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = ''
+          Additional arguments passed to agetty.
+        '';
+        example = [ "--nohostname" ];
       };
 
       greetingLine = mkOption {
diff --git a/nixos/modules/services/wayland/cage.nix b/nixos/modules/services/wayland/cage.nix
index 14d84c4ce0f..2e71abb69fc 100644
--- a/nixos/modules/services/wayland/cage.nix
+++ b/nixos/modules/services/wayland/cage.nix
@@ -93,6 +93,6 @@ in {
     systemd.defaultUnit = "graphical.target";
   };
 
-  meta.maintainers = with lib.maintainers; [ matthewbauer flokli ];
+  meta.maintainers = with lib.maintainers; [ matthewbauer ];
 
 }
diff --git a/nixos/modules/services/web-apps/bookstack.nix b/nixos/modules/services/web-apps/bookstack.nix
new file mode 100644
index 00000000000..34a31af9c9d
--- /dev/null
+++ b/nixos/modules/services/web-apps/bookstack.nix
@@ -0,0 +1,368 @@
+{ 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 $*
+  '';
+
+
+in {
+  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 AppKey.
+        Used for encryption where needed. Can be generated with <code>head -c 32 /dev/urandom| base64</code> and must be prefixed with <literal>base64:</literal>.
+      '';
+      example = "/run/keys/bookstack-appkey";
+      type = types.path;
+    };
+
+    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>
+      '';
+      example = "https://example.com";
+      type = types.str;
+    };
+
+    cacheDir = mkOption {
+      description = "BookStack cache directory";
+      default = "/var/cache/bookstack";
+      type = types.path;
+    };
+
+    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 = "\${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 = {
+        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.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      example = ''
+        ALLOWED_IFRAME_HOSTS="https://example.com"
+        WKHTMLTOPDF=/home/user/bins/wkhtmltopdf
+      '';
+      description = ''
+        Lines to be appended verbatim to the BookStack configuration.
+        Refer to <link xlink:href="https://www.bookstackapp.com/docs/"/> for details on supported values.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = db.createLocally -> db.user == user;
+        message = "services.bookstack.database.user must be set to ${user} if services.mediawiki.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.";
+      }
+    ];
+
+    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;
+      virtualHosts.bookstack = mkMerge [ cfg.nginx {
+        root = mkForce "${bookstack}/public";
+        extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;";
+        locations = {
+          "/" = {
+            index = "index.php";
+            extraConfig = ''try_files $uri $uri/ /index.php?$query_string;'';
+          };
+          "~ \.php$" = {
+            extraConfig = ''
+              try_files $uri $uri/ /index.php?$query_string;
+              include ${pkgs.nginx}/conf/fastcgi_params;
+              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+              fastcgi_param REDIRECT_STATUS 200;
+              fastcgi_pass unix:${config.services.phpfpm.pools."bookstack".socket};
+              ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"}
+            '';
+          };
+          "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
+            extraConfig = "expires 365d;";
+          };
+        };
+      }];
+    };
+
+    systemd.services.bookstack-setup = {
+      description = "Preperation tasks for BookStack";
+      before = [ "phpfpm-bookstack.service" ];
+      after = optional db.createLocally "mysql.service";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = user;
+        WorkingDirectory = "${bookstack}";
+      };
+      script = ''
+        # set permissions
+        umask 077
+        # create .env file
+        echo "
+        APP_KEY=base64:$(head -n1 ${cfg.appKeyFile})
+        APP_URL=${cfg.appURL}
+        DB_HOST=${db.host}
+        DB_PORT=${toString 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=${toString mail.port}
+        ${optionalString (mail.user != null) "MAIL_USERNAME=${mail.user};"}
+        ${optionalString (mail.encryption != null) "MAIL_ENCRYPTION=${mail.encryption};"}
+        ${optionalString (db.passwordFile != null) "DB_PASSWORD=$(head -n1 ${db.passwordFile})"}
+        ${optionalString (mail.passwordFile != null) "MAIL_PASSWORD=$(head -n1 ${mail.passwordFile})"}
+        APP_SERVICES_CACHE=${cfg.cacheDir}/services.php
+        APP_PACKAGES_CACHE=${cfg.cacheDir}/packages.php
+        APP_CONFIG_CACHE=${cfg.cacheDir}/config.php
+        APP_ROUTES_CACHE=${cfg.cacheDir}/routes-v7.php
+        APP_EVENTS_CACHE=${cfg.cacheDir}/events.php
+        ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "SESSION_SECURE_COOKIE=true"}
+        ${toString cfg.extraConfig}
+        " > "${cfg.dataDir}/.env"
+
+        # migrate db
+        ${pkgs.php}/bin/php artisan migrate --force
+
+        # clear & create caches (needed in case of update)
+        ${pkgs.php}/bin/php artisan cache:clear
+        ${pkgs.php}/bin/php artisan config:clear
+        ${pkgs.php}/bin/php artisan view:clear
+        ${pkgs.php}/bin/php artisan config:cache
+        ${pkgs.php}/bin/php artisan route:cache
+        ${pkgs.php}/bin/php artisan view:cache
+      '';
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${cfg.cacheDir}                           0700 ${user} ${group} - -"
+      "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/discourse.nix b/nixos/modules/services/web-apps/discourse.nix
new file mode 100644
index 00000000000..00b58d50257
--- /dev/null
+++ b/nixos/modules/services/web-apps/discourse.nix
@@ -0,0 +1,1037 @@
+{ config, options, lib, pkgs, utils, ... }:
+
+let
+  json = pkgs.formats.json {};
+
+  cfg = config.services.discourse;
+
+  postgresqlPackage = if config.services.postgresql.enable then
+                        config.services.postgresql.package
+                      else
+                        pkgs.postgresql;
+
+  # 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;
+        defaultText = "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 = "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 = "true, unless services.discourse.sslCertificate and services.discourse.sslCertificateKey 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.literalExample ''
+          {
+            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.literalExample ''
+          {
+            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 = {
+        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.
+          '';
+        };
+      };
+
+      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";
+          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 = ''
+            "notifications@`config.services.discourse.hostname`" if
+            config.services.discourse.mail.incoming.enable is "true",
+            otherwise "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.int;
+            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;
+            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
+            '';
+          };
+        };
+
+        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 = "%{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 = "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 = ''
+          [
+            (pkgs.fetchFromGitHub {
+              owner = "discourse";
+              repo = "discourse-spoiler-alert";
+              rev = "e200cfa571d252cab63f3d30d619b370986e4cee";
+              sha256 = "0ya69ix5g77wz4c9x9gmng6l25ghb5xxlx3icr6jam16q14dzc33";
+            })
+          ];
+        '';
+        description = ''
+          <productname>Discourse</productname> plugins to install as a
+          list of derivations. As long as a plugin supports the
+          standard install method, packaging it should only require
+          fetching its source with an appropriate fetcher.
+        '';
+      };
+
+      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.";
+      }
+    ];
+
+
+    # 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;
+
+      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_slave_host = null;
+      redis_slave_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_slave_host = null;
+      message_bus_redis_slave_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_key_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;
+      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;
+    };
+
+    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
+        cfg.package.rake
+      ];
+      environment = cfg.package.runtimeEnv // {
+        UNICORN_TIMEOUT = builtins.toString cfg.unicornTimeout;
+        UNICORN_SIDEKIQS = builtins.toString cfg.sidekiqProcesses;
+      };
+
+      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) ''
+              (
+                  password=$(<'${file}')
+                  replace-literal -fe '${file}' "$password" /run/discourse/config/discourse.conf
+              )
+            '';
+        in ''
+          set -o errexit -o pipefail -o nounset -o errtrace
+          shopt -s inherit_errexit
+
+          umask u=rwx,g=rx,o=
+
+          cp -r ${cfg.package}/share/discourse/config.dist/* /run/discourse/config/
+          cp -r ${cfg.package}/share/discourse/public.dist/* /run/discourse/public/
+          cp -r ${cfg.package}/share/discourse/plugins.dist/* /run/discourse/plugins/
+          ${lib.concatMapStringsSep "\n" (p: "ln -sf ${p} /run/discourse/plugins/") cfg.plugins}
+          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 0400 -o discourse ${discourseConf} /run/discourse/config/discourse.conf
+              ${mkSecretReplacement cfg.database.passwordFile}
+              ${mkSecretReplacement cfg.mail.outgoing.passwordFile}
+              ${mkSecretReplacement cfg.redis.passwordFile}
+              ${mkSecretReplacement cfg.secretKeyBaseFile}
+          )
+
+          discourse-rake db:migrate >>/var/log/discourse/db_migration.log
+          chmod -R u+w /run/discourse/tmp/
+
+          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
+
+          discourse-rake themes:update
+          discourse-rake uploads:regenerate_missing_optimized
+        '';
+
+      serviceConfig = {
+        Type = "simple";
+        User = "discourse";
+        Group = "discourse";
+        RuntimeDirectory = map (p: "discourse/" + p) [
+          "config"
+          "home"
+          "tmp"
+          "assets/javascripts/plugins"
+          "public"
+          "plugins"
+          "sockets"
+        ];
+        RuntimeDirectoryMode = 0750;
+        StateDirectory = map (p: "discourse/" + p) [
+          "uploads"
+          "backups"
+        ];
+        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 heirarchy 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 = "/run/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 /run/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 /run/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 /run/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..bae56242321
--- /dev/null
+++ b/nixos/modules/services/web-apps/discourse.xml
@@ -0,0 +1,323 @@
+<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.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. As long as a plugin supports the standard install
+      method, packaging it should only require fetching its source
+      with an appropriate fetcher.
+    </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>
+      plugin and disable it 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> = [
+    (pkgs.fetchFromGitHub {
+      owner = "discourse";
+      repo = "discourse-spoiler-alert";
+      rev = "e200cfa571d252cab63f3d30d619b370986e4cee";
+      sha256 = "0ya69ix5g77wz4c9x9gmng6l25ghb5xxlx3icr6jam16q14dzc33";
+    })
+  ];
+  <link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
+    plugins = {
+      spoiler_enabled = false;
+    };
+  };
+  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
+};
+</programlisting>
+
+    </para>
+  </section>
+</chapter>
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
index 9567223ebc7..685cb496703 100644
--- a/nixos/modules/services/web-apps/dokuwiki.nix
+++ b/nixos/modules/services/web-apps/dokuwiki.nix
@@ -193,7 +193,7 @@ let
                 };
                 sourceRoot = ".";
                 # We need unzip to build this package
-                buildInputs = [ pkgs.unzip ];
+                nativeBuildInputs = [ pkgs.unzip ];
                 # Installing simply means copying all files to the output directory
                 installPhase = "mkdir -p $out; cp -R * $out/";
               };
@@ -220,7 +220,7 @@ let
                   sha256 = "4de5ff31d54dd61bbccaf092c9e74c1af3a4c53e07aa59f60457a8f00cfb23a6";
                 };
                 # We need unzip to build this package
-                buildInputs = [ pkgs.unzip ];
+                nativeBuildInputs = [ pkgs.unzip ];
                 # Installing simply means copying all files to the output directory
                 installPhase = "mkdir -p $out; cp -R * $out/";
               };
@@ -329,7 +329,7 @@ in
           extraConfig = "internal;";
         };
 
-        locations."~ ^/lib.*\.(js|css|gif|png|ico|jpg|jpeg)$" = {
+        locations."~ ^/lib.*\\.(js|css|gif|png|ico|jpg|jpeg)$" = {
           extraConfig = "expires 365d;";
         };
 
@@ -349,7 +349,7 @@ in
           '';
         };
 
-        locations."~ \.php$" = {
+        locations."~ \\.php$" = {
           extraConfig = ''
               try_files $uri $uri/ /doku.php;
               include ${pkgs.nginx}/conf/fastcgi_params;
diff --git a/nixos/modules/services/web-apps/galene.nix b/nixos/modules/services/web-apps/galene.nix
index 769490e915a..dd63857a55c 100644
--- a/nixos/modules/services/web-apps/galene.nix
+++ b/nixos/modules/services/web-apps/galene.nix
@@ -133,8 +133,10 @@ in
       wantedBy = [ "multi-user.target" ];
 
       preStart = ''
-        install -m 700 -o '${cfg.user}' -g '${cfg.group}' ${cfg.certFile} ${cfg.dataDir}/cert.pem
-        install -m 700 -o '${cfg.user}' -g '${cfg.group}' ${cfg.keyFile} ${cfg.dataDir}/key.pem
+        ${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 [
diff --git a/nixos/modules/services/web-apps/hledger-web.nix b/nixos/modules/services/web-apps/hledger-web.nix
index 43fc4daa177..a69767194c3 100644
--- a/nixos/modules/services/web-apps/hledger-web.nix
+++ b/nixos/modules/services/web-apps/hledger-web.nix
@@ -26,19 +26,46 @@ in {
       '';
     };
 
-    capabilities = mkOption {
-      type = types.commas;
-      default = "view";
+    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 = ''
-        Enable the view, add, and/or manage capabilities. E.g. view,add
+        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.
       '';
     };
 
-    journalFile = mkOption {
-      type = types.path;
-      example = "/home/hledger/.hledger.journal";
+    journalFiles = mkOption {
+      type = types.listOf types.str;
+      default = [ ".hledger.journal" ];
       description = ''
-        Input journal file.
+        Paths to journal files relative to <option>services.hledger-web.stateDir</option>.
       '';
     };
 
@@ -50,28 +77,66 @@ in {
         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 {
-    systemd.services.hledger-web = {
+
+    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 = {
-        ExecStart = ''
-          ${pkgs.hledger-web}/bin/hledger-web \
-          --host=${cfg.host} \
-          --port=${toString cfg.port} \
-          --file=${cfg.journalFile}  \
-          "--capabilities=${cfg.capabilities}" \
-          ${optionalString (cfg.baseUrl != null) "--base-url=${cfg.baseUrl}"} \
-          ${optionalString (cfg.serveApi) "--serve-api"}
-        '';
-        Restart = "always";
-      };
+      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 ];
+  meta.maintainers = with lib.maintainers; [ marijanp erictapen ];
 }
diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
index a93e9327933..5b578cd8c4a 100644
--- a/nixos/modules/services/web-apps/keycloak.nix
+++ b/nixos/modules/services/web-apps/keycloak.nix
@@ -171,6 +171,12 @@ in
         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.databaseCreateLocally" /> to
+        <literal>false</literal> and create the database and user
+        manually. The database should be called
+        <literal>keycloak</literal>.
       '';
     };
 
diff --git a/nixos/modules/services/web-apps/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix
index ea7aebc3b12..af46f4e1927 100644
--- a/nixos/modules/services/web-apps/mastodon.nix
+++ b/nixos/modules/services/web-apps/mastodon.nix
@@ -31,6 +31,8 @@ let
   // (if cfg.smtp.authenticate then { SMTP_LOGIN  = cfg.smtp.user; } else {})
   // cfg.extraConfig;
 
+  systemCallsList = [ "@clock" "@cpu-emulation" "@debug" "@keyring" "@module" "@mount" "@obsolete" "@raw-io" "@reboot" "@setuid" "@swap" ];
+
   cfgService = {
     # User and group
     User = cfg.user;
@@ -43,8 +45,31 @@ let
     LogsDirectoryMode = "0750";
     # 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;
+    PrivateMounts = true;
+    # System Call Filtering
+    SystemCallArchitectures = "native";
   };
 
   envFile = pkgs.writeText "mastodon.env" (lib.concatMapStrings (s: s + "\n") (
@@ -408,6 +433,8 @@ in {
       serviceConfig = {
         Type = "oneshot";
         WorkingDirectory = cfg.package;
+        # System Call Filtering
+        SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]);
       } // cfgService;
 
       after = [ "network.target" ];
@@ -433,6 +460,8 @@ in {
         Type = "oneshot";
         EnvironmentFile = "/var/lib/mastodon/.secrets_env";
         WorkingDirectory = cfg.package;
+        # System Call Filtering
+        SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]);
       } // cfgService;
       after = [ "mastodon-init-dirs.service" "network.target" ] ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else []);
       wantedBy = [ "multi-user.target" ];
@@ -457,6 +486,8 @@ in {
         # Runtime directory and mode
         RuntimeDirectory = "mastodon-streaming";
         RuntimeDirectoryMode = "0750";
+        # System Call Filtering
+        SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@privileged" "@resources" ]);
       } // cfgService;
     };
 
@@ -479,6 +510,8 @@ in {
         # Runtime directory and mode
         RuntimeDirectory = "mastodon-web";
         RuntimeDirectoryMode = "0750";
+        # System Call Filtering
+        SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]);
       } // cfgService;
       path = with pkgs; [ file imagemagick ffmpeg ];
     };
@@ -498,6 +531,8 @@ in {
         RestartSec = 20;
         EnvironmentFile = "/var/lib/mastodon/.secrets_env";
         WorkingDirectory = cfg.package;
+        # System Call Filtering
+        SystemCallFilter = "~" + lib.concatStringsSep " " systemCallsList;
       } // cfgService;
       path = with pkgs; [ file imagemagick ffmpeg ];
     };
diff --git a/nixos/modules/services/web-apps/miniflux.nix b/nixos/modules/services/web-apps/miniflux.nix
index 62906d5e6a0..01710b1bd59 100644
--- a/nixos/modules/services/web-apps/miniflux.nix
+++ b/nixos/modules/services/web-apps/miniflux.nix
@@ -14,17 +14,16 @@ let
     ADMIN_PASSWORD=password
   '';
 
-  pgsu = "${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser}";
   pgbin = "${config.services.postgresql.package}/bin";
   preStart = pkgs.writeScript "miniflux-pre-start" ''
     #!${pkgs.runtimeShell}
     db_exists() {
-      [ "$(${pgsu} ${pgbin}/psql -Atc "select 1 from pg_database where datname='$1'")" == "1" ]
+      [ "$(${pgbin}/psql -Atc "select 1 from pg_database where datname='$1'")" == "1" ]
     }
     if ! db_exists "${dbName}"; then
-      ${pgsu} ${pgbin}/psql postgres -c "CREATE ROLE ${dbUser} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '${dbPassword}'"
-      ${pgsu} ${pgbin}/createdb --owner "${dbUser}" "${dbName}"
-      ${pgsu} ${pgbin}/psql "${dbName}" -c "CREATE EXTENSION IF NOT EXISTS hstore"
+      ${pgbin}/psql postgres -c "CREATE ROLE ${dbUser} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '${dbPassword}'"
+      ${pgbin}/createdb --owner "${dbUser}" "${dbName}"
+      ${pgbin}/psql "${dbName}" -c "CREATE EXTENSION IF NOT EXISTS hstore"
     fi
   '';
 in
@@ -73,15 +72,26 @@ in
 
     services.postgresql.enable = true;
 
+    systemd.services.miniflux-dbsetup = {
+      description = "Miniflux database setup";
+      wantedBy = [ "multi-user.target" ];
+      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 = [ "postgresql.service" ];
-      after = [ "network.target" "postgresql.service" ];
+      after = [ "network.target" "postgresql.service" "miniflux-dbsetup.service" ];
 
       serviceConfig = {
         ExecStart = "${pkgs.miniflux}/bin/miniflux";
-        ExecStartPre = "+${preStart}";
         DynamicUser = true;
         RuntimeDirectory = "miniflux";
         RuntimeDirectoryMode = "0700";
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index de1c67235f4..545deaa905f 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -10,7 +10,7 @@ let
     extensions = { enabled, all }:
       (with all;
         enabled
-        ++ [ imagick ] # Always enabled
+        ++ optional cfg.enableImagemagick imagick
         # Optionally enabled depending on caching settings
         ++ optional cfg.caching.apcu apcu
         ++ optional cfg.caching.redis redis
@@ -28,7 +28,10 @@ let
     upload_max_filesize = cfg.maxUploadSize;
     post_max_size = cfg.maxUploadSize;
     memory_limit = cfg.maxUploadSize;
-  } // cfg.phpOptions;
+  } // cfg.phpOptions
+    // optionalAttrs cfg.caching.apcu {
+      "apc.enable_cli" = "1";
+    };
 
   occ = pkgs.writeScriptBin "nextcloud-occ" ''
     #! ${pkgs.runtimeShell}
@@ -60,6 +63,9 @@ in {
       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 = {
@@ -86,7 +92,7 @@ in {
     package = mkOption {
       type = types.package;
       description = "Which package to use for the Nextcloud instance.";
-      relatedPackages = [ "nextcloud18" "nextcloud19" "nextcloud20" ];
+      relatedPackages = [ "nextcloud19" "nextcloud20" "nextcloud21" ];
     };
 
     maxUploadSize = mkOption {
@@ -280,6 +286,34 @@ in {
           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.
+        '';
+      };
+    };
+
+    enableImagemagick = mkEnableOption ''
+        Whether to load the ImageMagick module into 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 https://github.com/nextcloud/server/issues/13099
+    '' // {
+      default = true;
     };
 
     caching = {
@@ -345,10 +379,13 @@ in {
             && !(acfg.adminpass != null && acfg.adminpassFile != null));
           message = "Please specify exactly one of adminpass or adminpassFile";
         }
+        { assertion = versionOlder cfg.package.version "21" -> cfg.config.defaultPhoneRegion == null;
+          message = "The `defaultPhoneRegion'-setting is only supported for Nextcloud >=21!";
+        }
       ];
 
       warnings = let
-        latest = 20;
+        latest = 21;
         upgradeWarning = major: nixos:
           ''
             A legacy Nextcloud install (from before NixOS ${nixos}) may be installed.
@@ -366,9 +403,9 @@ in {
           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 "18") (upgradeWarning 17 "20.03"))
         ++ (optional (versionOlder cfg.package.version "19") (upgradeWarning 18 "20.09"))
-        ++ (optional (versionOlder cfg.package.version "20") (upgradeWarning 19 "21.05"));
+        ++ (optional (versionOlder cfg.package.version "20") (upgradeWarning 19 "21.05"))
+        ++ (optional (versionOlder cfg.package.version "21") (upgradeWarning 20 "21.05"));
 
       services.nextcloud.package = with pkgs;
         mkDefault (
@@ -378,14 +415,13 @@ in {
               nextcloud defined in an overlay, please set `services.nextcloud.package` to
               `pkgs.nextcloud`.
             ''
-          else if versionOlder stateVersion "20.03" then nextcloud17
           else if versionOlder stateVersion "20.09" then nextcloud18
           # 21.03 will not be an official release - it was instead 21.05.
           # This versionOlder statement remains set to 21.03 for backwards compatibility.
           # See https://github.com/NixOS/nixpkgs/pull/108899 and
           # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md.
           else if versionOlder stateVersion "21.03" then nextcloud19
-          else nextcloud20
+          else nextcloud21
         );
     }
 
@@ -443,6 +479,7 @@ in {
               'dbtype' => '${c.dbtype}',
               'trusted_domains' => ${writePhpArrary ([ cfg.hostName ] ++ c.extraTrustedDomains)},
               'trusted_proxies' => ${writePhpArrary (c.trustedProxies)},
+              ${optionalString (c.defaultPhoneRegion != null) "'default_phone_region' => '${c.defaultPhoneRegion}',"}
             ];
           '';
           occInstallCmd = let
@@ -571,6 +608,7 @@ in {
         home = "${cfg.home}";
         group = "nextcloud";
         createHome = true;
+        isSystemUser = true;
       };
       users.groups.nextcloud.members = [ "nextcloud" config.services.nginx.user ];
 
@@ -591,6 +629,14 @@ in {
               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;";
@@ -609,6 +655,9 @@ in {
               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;
             '';
           };
diff --git a/nixos/modules/services/web-apps/nextcloud.xml b/nixos/modules/services/web-apps/nextcloud.xml
index 6cbfda118c4..83a6f68edcb 100644
--- a/nixos/modules/services/web-apps/nextcloud.xml
+++ b/nixos/modules/services/web-apps/nextcloud.xml
@@ -11,7 +11,7 @@
   desktop client is packaged at <literal>pkgs.nextcloud-client</literal>.
  </para>
  <para>
-  The current default by NixOS is <package>nextcloud20</package> which is also the latest
+  The current default by NixOS is <package>nextcloud21</package> which is also the latest
   major version available.
  </para>
  <section xml:id="module-services-nextcloud-basic-usage">
diff --git a/nixos/modules/services/web-apps/shiori.nix b/nixos/modules/services/web-apps/shiori.nix
index 9083ddfa220..8f96dd9b5dd 100644
--- a/nixos/modules/services/web-apps/shiori.nix
+++ b/nixos/modules/services/web-apps/shiori.nix
@@ -87,8 +87,8 @@ in {
         SystemCallFilter = [
           "@system-service"
 
-          "~@chown" "~@cpu-emulation" "~@debug" "~@ipc" "~@keyring" "~@memlock"
-          "~@module" "~@obsolete" "~@privileged" "~@process" "~@raw-io"
+          "~@chown" "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock"
+          "~@module" "~@obsolete" "~@privileged" "~@raw-io"
           "~@resources" "~@setuid"
         ];
       };
diff --git a/nixos/modules/services/web-apps/whitebophir.nix b/nixos/modules/services/web-apps/whitebophir.nix
index a19812547c4..b265296d5c1 100644
--- a/nixos/modules/services/web-apps/whitebophir.nix
+++ b/nixos/modules/services/web-apps/whitebophir.nix
@@ -16,6 +16,12 @@ in {
         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;
@@ -30,7 +36,8 @@ in {
       wantedBy    = [ "multi-user.target" ];
       after       = [ "network.target" ];
       environment = {
-        PORT            = "${toString cfg.port}";
+        PORT            = toString cfg.port;
+        HOST            = toString cfg.listenAddress;
         WBO_HISTORY_DIR = "/var/lib/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
index 5fbe53221ae..f251cfe32db 100644
--- a/nixos/modules/services/web-apps/wordpress.nix
+++ b/nixos/modules/services/web-apps/wordpress.nix
@@ -109,7 +109,7 @@ let
                 sha256 = "1rhba5h5fjlhy8p05zf0p14c9iagfh96y91r36ni0rmk6y891lyd";
               };
               # We need unzip to build this package
-              buildInputs = [ pkgs.unzip ];
+              nativeBuildInputs = [ pkgs.unzip ];
               # Installing simply means copying all files to the output directory
               installPhase = "mkdir -p $out; cp -R * $out/";
             };
@@ -136,7 +136,7 @@ let
                 sha256 = "0rjwm811f4aa4q43r77zxlpklyb85q08f9c8ns2akcarrvj5ydx3";
               };
               # We need unzip to build this package
-              buildInputs = [ pkgs.unzip ];
+              nativeBuildInputs = [ pkgs.unzip ];
               # Installing simply means copying all files to the output directory
               installPhase = "mkdir -p $out; cp -R * $out/";
             };
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index 7f50b8fd8d4..b2bb5055cd4 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -22,7 +22,9 @@ let
 
   php = cfg.phpPackage.override { apacheHttpd = pkg; };
 
-  phpMajorVersion = lib.versions.major (lib.getVersion php);
+  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; };
 
@@ -63,7 +65,7 @@ let
     ++ 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 = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.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;
 
diff --git a/nixos/modules/services/web-servers/minio.nix b/nixos/modules/services/web-servers/minio.nix
index cd123000f00..381a55faff1 100644
--- a/nixos/modules/services/web-servers/minio.nix
+++ b/nixos/modules/services/web-servers/minio.nix
@@ -18,9 +18,9 @@ in
     };
 
     dataDir = mkOption {
-      default = "/var/lib/minio/data";
-      type = types.path;
-      description = "The data directory, for storing the objects.";
+      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 {
@@ -74,15 +74,14 @@ in
   config = mkIf cfg.enable {
     systemd.tmpfiles.rules = [
       "d '${cfg.configDir}' - minio minio - -"
-      "d '${cfg.dataDir}' - 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} --config-dir=${cfg.configDir} ${cfg.dataDir}";
+        ExecStart = "${cfg.package}/bin/minio server --json --address ${cfg.listenAddress} --config-dir=${cfg.configDir} ${toString cfg.dataDir}";
         Type = "simple";
         User = "minio";
         Group = "minio";
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index fa8614e8ec1..d811879b7b1 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -154,10 +154,10 @@ let
 
       ${optionalString (cfg.recommendedProxySettings) ''
         proxy_redirect          off;
-        proxy_connect_timeout   90;
-        proxy_send_timeout      90;
-        proxy_read_timeout      90;
-        proxy_http_version      1.0;
+        proxy_connect_timeout   60;
+        proxy_send_timeout      60;
+        proxy_read_timeout      60;
+        proxy_http_version      1.1;
         include ${recommendedProxyConfig};
       ''}
 
@@ -249,7 +249,15 @@ let
           + 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;
 
@@ -397,6 +405,9 @@ in
         default = pkgs.nginxStable;
         defaultText = "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
@@ -404,6 +415,17 @@ in
         ";
       };
 
+      additionalModules = mkOption {
+        default = [];
+        type = types.listOf (types.attrsOf types.anything);
+        example = literalExample "[ 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;
@@ -662,6 +684,7 @@ in
                 Defines the address and other parameters of the upstream servers.
               '';
               default = {};
+              example = { "127.0.0.1:8000" = {}; };
             };
             extraConfig = mkOption {
               type = types.lines;
@@ -676,6 +699,14 @@ in
           Defines a group of servers to use as proxy target.
         '';
         default = {};
+        example = literalExample ''
+          "backend_server" = {
+            servers = { "127.0.0.1:8000" = {}; };
+            extraConfig = ''''
+              keepalive 16;
+            '''';
+          };
+        '';
       };
 
       virtualHosts = mkOption {
@@ -788,28 +819,38 @@ in
         # 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
+        # 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)) pkgs.nginx.modules);
+        MemoryDenyWriteExecute = !(builtins.any (mod: (mod.allowMemoryWriteExecute or false)) cfg.package.modules);
         RestrictRealtime = true;
         RestrictSUIDSGID = true;
+        RemoveIPC = true;
         PrivateMounts = true;
         # System Call Filtering
         SystemCallArchitectures = "native";
+        SystemCallFilter = "~@chown @cpu-emulation @debug @keyring @ipc @module @mount @obsolete @privileged @raw-io @reboot @setuid @swap";
       };
     };
 
@@ -856,6 +897,7 @@ in
     users.users = optionalAttrs (cfg.user == "nginx") {
       nginx = {
         group = cfg.group;
+        isSystemUser = true;
         uid = config.ids.uids.nginx;
       };
     };
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
index cf211ea9a71..1f5fe6a368c 100644
--- a/nixos/modules/services/web-servers/nginx/vhost-options.nix
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -151,6 +151,19 @@ with lib;
       '';
     };
 
+    http3 = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable HTTP 3.
+        This requires using <literal>pkgs.nginxQuic</literal> package
+        which can be achived 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;
diff --git a/nixos/modules/services/web-servers/pomerium.nix b/nixos/modules/services/web-servers/pomerium.nix
new file mode 100644
index 00000000000..2bc7d01c7c2
--- /dev/null
+++ b/nixos/modules/services/web-servers/pomerium.nix
@@ -0,0 +1,131 @@
+{ 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;
+
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = [ "pomerium" ];
+        ExecStart = "${pkgs.pomerium}/bin/pomerium -config ${cfgFile}";
+
+        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" ];
+
+        WorkingDirectory = mkIf (cfg.useACMEHost != null) "$CREDENTIALS_DIRECTORY";
+        LoadCredential = optionals (cfg.useACMEHost != null) [
+          "fullchain.pem:/var/lib/acme/${cfg.useACMEHost}/fullchain.pem"
+          "key.pem:/var/lib/acme/${cfg.useACMEHost}/key.pem"
+        ];
+      };
+    };
+
+    # 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 restart pomerium.service";
+      };
+    };
+  });
+}
diff --git a/nixos/modules/services/web-servers/trafficserver.nix b/nixos/modules/services/web-servers/trafficserver.nix
new file mode 100644
index 00000000000..db0e2ac0bd0
--- /dev/null
+++ b/nixos/modules/services/web-servers/trafficserver.nix
@@ -0,0 +1,318 @@
+{ 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";
+  getConfPath = name: "${pkgs.trafficserver}/etc/trafficserver/${name}";
+
+  yaml = pkgs.formats.yaml { };
+
+  fromYAML = f:
+    let
+      jsonFile = pkgs.runCommand "in.json"
+        {
+          nativeBuildInputs = [ pkgs.remarshal ];
+        } ''
+        yaml2json < "${f}" > "$out"
+      '';
+    in
+    builtins.fromJSON (builtins.readFile jsonFile);
+
+  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 = fromYAML (getConfPath "ip_allow.yaml");
+      defaultText = "upstream defaults";
+      example = literalExample {
+        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 = fromYAML (getConfPath "logging.yaml");
+      defaultText = "upstream defaults";
+      example = literalExample { };
+      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 = literalExample { 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 = literalExample {
+        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/x11/desktop-managers/gnome3.nix b/nixos/modules/services/x11/desktop-managers/gnome3.nix
index 99e6edfba26..81203c7622a 100644
--- a/nixos/modules/services/x11/desktop-managers/gnome3.nix
+++ b/nixos/modules/services/x11/desktop-managers/gnome3.nix
@@ -273,6 +273,7 @@ in
       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.gnome3.at-spi2-core.enable = true;
       services.gnome3.evolution-data-server.enable = true;
       services.gnome3.gnome-keyring.enable = true;
@@ -365,10 +366,10 @@ in
         gnome-bluetooth
         gnome-color-manager
         gnome-control-center
-        gnome-getting-started-docs
         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
diff --git a/nixos/modules/services/x11/desktop-managers/kodi.nix b/nixos/modules/services/x11/desktop-managers/kodi.nix
index bdae9c3afdb..af303d6fb27 100644
--- a/nixos/modules/services/x11/desktop-managers/kodi.nix
+++ b/nixos/modules/services/x11/desktop-managers/kodi.nix
@@ -14,6 +14,16 @@ in
         default = false;
         description = "Enable the kodi multimedia center.";
       };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.kodi;
+        defaultText = "pkgs.kodi";
+        example = "pkgs.kodi.withPackages (p: with p; [ jellyfin pvr-iptvsimple vfs-sftp ])";
+        description = ''
+          Package that should be used for Kodi.
+        '';
+      };
     };
   };
 
@@ -21,11 +31,11 @@ in
     services.xserver.desktopManager.session = [{
       name = "kodi";
       start = ''
-        LIRC_SOCKET_PATH=/run/lirc/lircd ${pkgs.kodi}/bin/kodi --standalone &
+        LIRC_SOCKET_PATH=/run/lirc/lircd ${cfg.package}/bin/kodi --standalone &
         waitPID=$!
       '';
     }];
 
-    environment.systemPackages = [ pkgs.kodi ];
+    environment.systemPackages = [ cfg.package ];
   };
 }
diff --git a/nixos/modules/services/x11/desktop-managers/xfce.nix b/nixos/modules/services/x11/desktop-managers/xfce.nix
index d39b4d64904..7d2856939c8 100644
--- a/nixos/modules/services/x11/desktop-managers/xfce.nix
+++ b/nixos/modules/services/x11/desktop-managers/xfce.nix
@@ -58,7 +58,7 @@ in
       noDesktop = mkOption {
         type = types.bool;
         default = false;
-        description = "Don't install XFCE desktop components (xfdesktop, panel and notification daemon).";
+        description = "Don't install XFCE desktop components (xfdesktop and panel).";
       };
 
       enableXfwm = mkOption {
@@ -98,6 +98,7 @@ in
       parole
       ristretto
       xfce4-appfinder
+      xfce4-notifyd
       xfce4-screenshooter
       xfce4-session
       xfce4-settings
@@ -119,7 +120,6 @@ in
         xfwm4
         xfwm4-themes
       ] ++ optionals (!cfg.noDesktop) [
-        xfce4-notifyd
         xfce4-panel
         xfdesktop
       ];
@@ -151,7 +151,6 @@ in
     services.upower.enable = config.powerManagement.enable;
     services.gnome3.glib-networking.enable = true;
     services.gvfs.enable = true;
-    services.gvfs.package = pkgs.xfce.gvfs;
     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
@@ -166,7 +165,8 @@ in
     # Systemd services
     systemd.packages = with pkgs.xfce; [
       (thunar.override { thunarPlugins = cfg.thunarPlugins; })
-    ] ++ optional (!cfg.noDesktop) xfce4-notifyd;
+      xfce4-notifyd
+    ];
 
   };
 }
diff --git a/nixos/modules/services/x11/display-managers/default.nix b/nixos/modules/services/x11/display-managers/default.nix
index 9fdbe753dad..e04fcdaf414 100644
--- a/nixos/modules/services/x11/display-managers/default.nix
+++ b/nixos/modules/services/x11/display-managers/default.nix
@@ -37,6 +37,11 @@ let
       . /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
@@ -64,22 +69,23 @@ let
 
       # Speed up application start by 50-150ms according to
       # http://kdemonkey.blogspot.nl/2008/04/magic-trick.html
-      rm -rf "$HOME/.compose-cache"
-      mkdir "$HOME/.compose-cache"
+      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 "$HOME/.local/share"
+      mkdir -p "''${XDG_DATA_HOME:-$HOME/.local/share}"
 
       unset _DID_SYSTEMD_CAT
 
       ${cfg.displayManager.sessionCommands}
 
-      # Allow the user to execute commands at the beginning of the X session.
-      if test -f ~/.xprofile; then
-          source ~/.xprofile
-      fi
-
       # Start systemd user services for graphical sessions
       /run/current-system/systemd/bin/systemctl --user start graphical-session.target
 
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index f79eb64b5a6..a214e91cfd3 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -218,14 +218,14 @@ in
     # We duplicate upstream's udev rules manually to make wayland with nvidia configurable
     services.udev.extraRules = ''
       # disable Wayland on Cirrus chipsets
-      ATTR{vendor}=="0x1013", ATTR{device}=="0x00b8", ATTR{subsystem_vendor}=="0x1af4", ATTR{subsystem_device}=="0x1100", RUN+="${gdm}/libexec/gdm-disable-wayland"
+      ATTR{vendor}=="0x1013", ATTR{device}=="0x00b8", ATTR{subsystem_vendor}=="0x1af4", ATTR{subsystem_device}=="0x1100", RUN+="${gdm}/libexec/gdm-runtime-config set daemon WaylandEnable false"
       # disable Wayland on Hi1710 chipsets
-      ATTR{vendor}=="0x19e5", ATTR{device}=="0x1711", RUN+="${gdm}/libexec/gdm-disable-wayland"
+      ATTR{vendor}=="0x19e5", ATTR{device}=="0x1711", RUN+="${gdm}/libexec/gdm-runtime-config set daemon WaylandEnable false"
       ${optionalString (!cfg.gdm.nvidiaWayland) ''
-        DRIVER=="nvidia", RUN+="${gdm}/libexec/gdm-disable-wayland"
+        DRIVER=="nvidia", RUN+="${gdm}/libexec/gdm-runtime-config set daemon WaylandEnable false"
       ''}
       # disable Wayland when modesetting is disabled
-      IMPORT{cmdline}="nomodeset", RUN+="${gdm}/libexec/gdm-disable-wayland"
+      IMPORT{cmdline}="nomodeset", RUN+="${gdm}/libexec/gdm-runtime-config set daemon WaylandEnable false"
     '';
 
     systemd.user.services.dbus.wantedBy = [ "default.target" ];
diff --git a/nixos/modules/services/x11/window-managers/default.nix b/nixos/modules/services/x11/window-managers/default.nix
index 9ca24310e56..53285fbce87 100644
--- a/nixos/modules/services/x11/window-managers/default.nix
+++ b/nixos/modules/services/x11/window-managers/default.nix
@@ -15,6 +15,7 @@ in
     ./cwm.nix
     ./clfswm.nix
     ./dwm.nix
+    ./e16.nix
     ./evilwm.nix
     ./exwm.nix
     ./fluxbox.nix
@@ -37,6 +38,7 @@ in
     ./tinywm.nix
     ./twm.nix
     ./windowmaker.nix
+    ./wmderland.nix
     ./wmii.nix
     ./xmonad.nix
     ./yeahwm.nix
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/wmderland.nix b/nixos/modules/services/x11/window-managers/wmderland.nix
new file mode 100644
index 00000000000..a6864a82771
--- /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
+      ];
+      example = literalExample ''
+        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/xserver.nix b/nixos/modules/services/x11/xserver.nix
index 8858559d8f2..4dde4476d2c 100644
--- a/nixos/modules/services/x11/xserver.nix
+++ b/nixos/modules/services/x11/xserver.nix
@@ -251,11 +251,10 @@ in
 
       videoDrivers = mkOption {
         type = types.listOf types.str;
-        # !!! We'd like "nv" here, but it segfaults the X server.
-        default = [ "radeon" "cirrus" "vesa" "modesetting" ];
+        default = [ "amdgpu" "radeon" "nouveau" "modesetting" "fbdev" ];
         example = [
-          "ati_unfree" "amdgpu" "amdgpu-pro"
-          "nv" "nvidia" "nvidiaLegacy390" "nvidiaLegacy340" "nvidiaLegacy304"
+          "nvidia" "nvidiaLegacy390" "nvidiaLegacy340" "nvidiaLegacy304"
+          "amdgpu-pro"
         ];
         # TODO(@oxij): think how to easily add the rest, like those nvidia things
         relatedPackages = concatLists
@@ -667,6 +666,7 @@ in
     # 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";
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index b82d69b3bb8..8bd85465472 100644
--- a/nixos/modules/system/activation/switch-to-configuration.pl
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -1,4 +1,4 @@
-#! @perl@
+#! @perl@/bin/perl
 
 use strict;
 use warnings;
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index b0f77ca3fb8..6751ca3f2ee 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, modules, baseModules, ... }:
+{ config, lib, pkgs, modules, baseModules, specialArgs, ... }:
 
 with lib;
 
@@ -13,7 +13,7 @@ let
   # !!! fix this
   children = mapAttrs (childName: childConfig:
       (import ../../../lib/eval-config.nix {
-        inherit baseModules;
+        inherit baseModules specialArgs;
         system = config.nixpkgs.initialSystem;
         modules =
            (optionals childConfig.inheritParentConfig modules)
@@ -113,8 +113,7 @@ let
     configurationName = config.boot.loader.grub.configurationName;
 
     # Needed by switch-to-configuration.
-
-    perl = "${pkgs.perl}/bin/perl " + (concatMapStringsSep " " (lib: "-I${lib}/${pkgs.perl.libPrefix}") (with pkgs.perlPackages; [ FileSlurp NetDBus XMLParser XMLTwig ]));
+    perl = pkgs.perl.withPackages (p: with p; [ FileSlurp NetDBus XMLParser XMLTwig ]);
   };
 
   # Handle assertions and warnings
diff --git a/nixos/modules/system/boot/initrd-openvpn.nix b/nixos/modules/system/boot/initrd-openvpn.nix
index e59bc7b6678..b35fb0b57c0 100644
--- a/nixos/modules/system/boot/initrd-openvpn.nix
+++ b/nixos/modules/system/boot/initrd-openvpn.nix
@@ -55,7 +55,7 @@ in
     # The shared libraries are required for DNS resolution
     boot.initrd.extraUtilsCommands = ''
       copy_bin_and_libs ${pkgs.openvpn}/bin/openvpn
-      copy_bin_and_libs ${pkgs.iproute}/bin/ip
+      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
diff --git a/nixos/modules/system/boot/kernel_config.nix b/nixos/modules/system/boot/kernel_config.nix
index 783685c9dfe..5d9534024b0 100644
--- a/nixos/modules/system/boot/kernel_config.nix
+++ b/nixos/modules/system/boot/kernel_config.nix
@@ -2,24 +2,6 @@
 
 with lib;
 let
-  findWinner = candidates: winner:
-    any (x: x == winner) candidates;
-
-  # winners is an ordered list where first item wins over 2nd etc
-  mergeAnswer = winners: locs: defs:
-    let
-      values = map (x: x.value) defs;
-      inter = intersectLists values winners;
-      winner = head winners;
-    in
-    if defs == [] then abort "This case should never happen."
-    else if winner == [] then abort "Give a valid list of winner"
-    else if inter == [] then mergeOneOption locs defs
-    else if findWinner values winner then
-      winner
-    else
-      mergeAnswer (tail winners) locs defs;
-
   mergeFalseByDefault = locs: defs:
     if defs == [] then abort "This case should never happen."
     else if any (x: x == false) (getValues defs) then false
@@ -28,9 +10,7 @@ let
   kernelItem = types.submodule {
     options = {
       tristate = mkOption {
-        type = types.enum [ "y" "m" "n" null ] // {
-          merge = mergeAnswer [ "y" "m" "n" ];
-        };
+        type = types.enum [ "y" "m" "n" null ];
         default = null;
         internal = true;
         visible = true;
diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix
index 289c2b19986..c6ec9acd54c 100644
--- a/nixos/modules/system/boot/loader/grub/grub.nix
+++ b/nixos/modules/system/boot/loader/grub/grub.nix
@@ -728,13 +728,17 @@ in
             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
-        export PERL5LIB=${with pkgs.perlPackages; makePerlPath [ FileSlurp FileCopyRecursive XMLLibXML XMLSAX XMLSAXBase ListCompare JSON ]}
         ${optionalString cfg.enableCryptodisk "export GRUB_ENABLE_CRYPTODISK=y"}
       '' + flip concatMapStrings cfg.mirroredBoots (args: ''
-        ${pkgs.perl}/bin/perl ${install-grub-pl} ${grubConfig args} $@
+        ${perl}/bin/perl ${install-grub-pl} ${grubConfig args} $@
       '') + cfg.extraInstallCommands);
 
       system.build.grub = grub;
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
index 6bee900c683..63e01dd054a 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
@@ -15,12 +15,15 @@ import re
 import datetime
 import glob
 import os.path
+from typing import Tuple, List, Optional
 
-def copy_if_not_exists(source, dest):
+
+def copy_if_not_exists(source: str, dest: str) -> None:
     if not os.path.exists(dest):
         shutil.copyfile(source, dest)
 
-def system_dir(profile, generation):
+
+def system_dir(profile: Optional[str], generation: int) -> str:
     if profile:
         return "/nix/var/nix/profiles/system-profiles/%s-%d-link" % (profile, generation)
     else:
@@ -42,7 +45,8 @@ MEMTEST_BOOT_ENTRY = """title MemTest86
 efi /efi/memtest86/BOOTX64.efi
 """
 
-def write_loader_conf(profile, generation):
+
+def write_loader_conf(profile: Optional[str], generation: int) -> None:
     with open("@efiSysMountPoint@/loader/loader.conf.tmp", 'w') as f:
         if "@timeout@" != "":
             f.write("timeout @timeout@\n")
@@ -55,10 +59,12 @@ def write_loader_conf(profile, generation):
         f.write("console-mode @consoleMode@\n");
     os.rename("@efiSysMountPoint@/loader/loader.conf.tmp", "@efiSysMountPoint@/loader/loader.conf")
 
-def profile_path(profile, generation, name):
+
+def profile_path(profile: Optional[str], generation: int, name: str) -> str:
     return os.readlink("%s/%s" % (system_dir(profile, generation), name))
 
-def copy_from_profile(profile, generation, name, dry_run=False):
+
+def copy_from_profile(profile: Optional[str], generation: int, name: str, dry_run: bool = False) -> str:
     store_file_path = profile_path(profile, generation, name)
     suffix = os.path.basename(store_file_path)
     store_dir = os.path.basename(os.path.dirname(store_file_path))
@@ -67,7 +73,8 @@ def copy_from_profile(profile, generation, name, dry_run=False):
         copy_if_not_exists(store_file_path, "@efiSysMountPoint@%s" % (efi_file_path))
     return efi_file_path
 
-def describe_generation(generation_dir):
+
+def describe_generation(generation_dir: str) -> str:
     try:
         with open("%s/nixos-version" % generation_dir) as f:
             nixos_version = f.read()
@@ -87,7 +94,8 @@ def describe_generation(generation_dir):
 
     return description
 
-def write_entry(profile, generation, machine_id):
+
+def write_entry(profile: Optional[str], generation: int, machine_id: str) -> None:
     kernel = copy_from_profile(profile, generation, "kernel")
     initrd = copy_from_profile(profile, generation, "initrd")
     try:
@@ -116,14 +124,16 @@ def write_entry(profile, generation, machine_id):
             f.write("machine-id %s\n" % machine_id)
     os.rename(tmp_path, entry_file)
 
-def mkdir_p(path):
+
+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=None):
+
+def get_generations(profile: Optional[str] = None) -> List[Tuple[Optional[str], int]]:
     gen_list = subprocess.check_output([
         "@nix@/bin/nix-env",
         "--list-generations",
@@ -137,7 +147,8 @@ def get_generations(profile=None):
     configurationLimit = @configurationLimit@
     return [ (profile, int(line.split()[0])) for line in gen_lines ][-configurationLimit:]
 
-def remove_old_entries(gens):
+
+def remove_old_entries(gens: List[Tuple[Optional[str], int]]) -> None:
     rex_profile = re.compile("^@efiSysMountPoint@/loader/entries/nixos-(.*)-generation-.*\.conf$")
     rex_generation = re.compile("^@efiSysMountPoint@/loader/entries/nixos.*-generation-(.*)\.conf$")
     known_paths = []
@@ -150,8 +161,8 @@ def remove_old_entries(gens):
                 prof = rex_profile.sub(r"\1", path)
             else:
                 prof = "system"
-            gen = int(rex_generation.sub(r"\1", path))
-            if not (prof, gen) in gens:
+            gen_number = int(rex_generation.sub(r"\1", path))
+            if not (prof, gen_number) in gens:
                 os.unlink(path)
         except ValueError:
             pass
@@ -159,7 +170,8 @@ def remove_old_entries(gens):
         if not path in known_paths and not os.path.isdir(path):
             os.unlink(path)
 
-def get_profiles():
+
+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/")
@@ -167,7 +179,8 @@ def get_profiles():
     else:
         return []
 
-def main():
+
+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()
@@ -182,7 +195,9 @@ def main():
         # 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.check_output(cmd).rstrip()
+        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)
@@ -213,7 +228,6 @@ def main():
                 print("updating systemd-boot from %s to %s" % (sdboot_version, systemd_version))
                 subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "update"])
 
-
     mkdir_p("@efiSysMountPoint@/efi/nixos")
     mkdir_p("@efiSysMountPoint@/loader/entries")
 
@@ -222,9 +236,12 @@ def main():
         gens += get_generations(profile)
     remove_old_entries(gens)
     for gen in gens:
-        write_entry(*gen, machine_id)
-        if os.readlink(system_dir(*gen)) == args.default_config:
-            write_loader_conf(*gen)
+        try:
+            write_entry(*gen, machine_id)
+            if os.readlink(system_dir(*gen)) == args.default_config:
+                write_loader_conf(*gen)
+        except OSError as e:
+            print("ignoring profile '{}' in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr)
 
     memtest_entry_file = "@efiSysMountPoint@/loader/entries/memtest86.conf"
     if os.path.exists(memtest_entry_file):
@@ -252,5 +269,6 @@ def main():
     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
index f0bd76a3c1d..ff304f570d3 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
@@ -7,7 +7,7 @@ let
 
   efi = config.boot.loader.efi;
 
-  gummibootBuilder = pkgs.substituteAll {
+  systemdBootBuilder = pkgs.substituteAll {
     src = ./systemd-boot-builder.py;
 
     isExecutable = true;
@@ -30,6 +30,17 @@ let
 
     memtest86 = if cfg.memtest86.enable then pkgs.memtest86-efi else "";
   };
+
+  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 =
@@ -131,7 +142,7 @@ in {
     boot.loader.supportsInitrdSecrets = true;
 
     system = {
-      build.installBootLoader = gummibootBuilder;
+      build.installBootLoader = checkedSystemdBootBuilder;
 
       boot.loader.id = "systemd-boot";
 
diff --git a/nixos/modules/system/boot/plymouth.nix b/nixos/modules/system/boot/plymouth.nix
index ef916899944..2a545e55251 100644
--- a/nixos/modules/system/boot/plymouth.nix
+++ b/nixos/modules/system/boot/plymouth.nix
@@ -4,8 +4,7 @@ with lib;
 
 let
 
-  inherit (pkgs) plymouth;
-  inherit (pkgs) nixos-icons;
+  inherit (pkgs) plymouth nixos-icons;
 
   cfg = config.boot.plymouth;
 
@@ -16,14 +15,37 @@ let
     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 ] ++ cfg.themePackages;
+    paths = [
+      plymouth
+      plymouthLogos
+    ] ++ cfg.themePackages;
   };
 
   configFile = pkgs.writeText "plymouthd.conf" ''
     [Daemon]
     ShowDelay=0
+    DeviceTimeout=8
     Theme=${cfg.theme}
     ${cfg.extraConfig}
   '';
@@ -47,7 +69,7 @@ in
       };
 
       themePackages = mkOption {
-        default = [ nixosBreezePlymouth ];
+        default = lib.optional (cfg.theme == "breeze") nixosBreezePlymouth;
         type = types.listOf types.package;
         description = ''
           Extra theme packages for plymouth.
@@ -55,7 +77,7 @@ in
       };
 
       theme = mkOption {
-        default = "breeze";
+        default = "bgrt";
         type = types.str;
         description = ''
           Splash screen theme.
@@ -64,7 +86,8 @@ in
 
       logo = mkOption {
         type = types.path;
-        default = "${nixos-icons}/share/icons/hicolor/128x128/apps/nix-snowflake.png";
+        # Dimensions are 48x48 to match GDM logo
+        default = "${nixos-icons}/share/icons/hicolor/48x48/apps/nix-snowflake-white.png";
         defaultText = ''pkgs.fetchurl {
           url = "https://nixos.org/logo/nixos-hires.png";
           sha256 = "1ivzgd7iz0i06y36p8m5w48fd8pjqwxhdaavc0pxs7w1g7mcy5si";
@@ -110,12 +133,18 @@ in
     systemd.services.plymouth-poweroff.wantedBy = [ "poweroff.target" ];
     systemd.services.plymouth-reboot.wantedBy = [ "reboot.target" ];
     systemd.services.plymouth-read-write.wantedBy = [ "sysinit.target" ];
-    systemd.services.systemd-ask-password-plymouth.wantedBy = ["multi-user.target"];
-    systemd.paths.systemd-ask-password-plymouth.wantedBy = ["multi-user.target"];
+    systemd.services.systemd-ask-password-plymouth.wantedBy = [ "multi-user.target" ];
+    systemd.paths.systemd-ask-password-plymouth.wantedBy = [ "multi-user.target" ];
 
     boot.initrd.extraUtilsCommands = ''
-      copy_bin_and_libs ${pkgs.plymouth}/bin/plymouthd
-      copy_bin_and_libs ${pkgs.plymouth}/bin/plymouth
+      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)"
 
@@ -127,21 +156,29 @@ in
       mkdir -p $out/share/plymouth/themes
       cp ${plymouth}/share/plymouth/plymouthd.defaults $out/share/plymouth
 
-      # copy themes into working directory for patching
+      # Copy themes into working directory for patching
       mkdir themes
-      # use -L to copy the directories proper, not the symlinks to them
-      cp -r -L ${themesEnv}/share/plymouth/themes/{text,details,${cfg.theme}} themes
 
-      # patch out any attempted references to the theme or plymouth's themes directory
+      # 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
-      cp ${cfg.logo} $out/share/plymouth/logo.png
 
+      # 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
diff --git a/nixos/modules/system/boot/resolved.nix b/nixos/modules/system/boot/resolved.nix
index 7fe8f4dfb7e..84bc9b78076 100644
--- a/nixos/modules/system/boot/resolved.nix
+++ b/nixos/modules/system/boot/resolved.nix
@@ -1,4 +1,4 @@
-{ config, pkgs, lib, ... }:
+{ config, lib, ... }:
 
 with lib;
 let
@@ -150,9 +150,6 @@ in
       wantedBy = [ "multi-user.target" ];
       aliases = [ "dbus-org.freedesktop.resolve1.service" ];
       restartTriggers = [ config.environment.etc."systemd/resolved.conf".source ];
-      # Upstream bug: https://github.com/systemd/systemd/issues/18078
-      # systemd-resolved without libidn2 is broken
-      environment.LD_LIBRARY_PATH = "${lib.getLib pkgs.libidn2}/lib";
     };
 
     environment.etc = {
diff --git a/nixos/modules/system/boot/stage-1-init.sh b/nixos/modules/system/boot/stage-1-init.sh
index 5b39f34200c..ddaf985878e 100644
--- a/nixos/modules/system/boot/stage-1-init.sh
+++ b/nixos/modules/system/boot/stage-1-init.sh
@@ -614,11 +614,16 @@ echo /sbin/modprobe > /proc/sys/kernel/modprobe
 
 
 # Start stage 2.  `switch_root' deletes all files in the ramfs on the
-# current root.  Note that $stage2Init might be an absolute symlink,
-# in which case "-e" won't work because we're not in the chroot yet.
-if [ ! -e "$targetRoot/$stage2Init" ] && [ ! -L "$targetRoot/$stage2Init" ] ; then
-    echo "stage 2 init script ($targetRoot/$stage2Init) not found"
-    fail
+# 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
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
index 4074f2e0235..d606d473d91 100644
--- a/nixos/modules/system/boot/stage-1.nix
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -386,7 +386,7 @@ let
           ) config.boot.initrd.secrets)
          }
 
-        (cd "$tmp" && find . -print0 | sort -z | cpio -o -H newc -R +0:+0 --reproducible --null) | \
+        (cd "$tmp" && find . -print0 | sort -z | cpio --quiet -o -H newc -R +0:+0 --reproducible --null) | \
           ${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1"
       '';
 
diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh
index 936077b9df1..50ee0b8841e 100644
--- a/nixos/modules/system/boot/stage-2-init.sh
+++ b/nixos/modules/system/boot/stage-2-init.sh
@@ -167,6 +167,7 @@ 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 \
+    LOCALE_ARCHIVE=/run/current-system/sw/lib/locale/locale-archive @systemdUnitPathEnvVar@ \
     exec @systemdExecutable@
diff --git a/nixos/modules/system/boot/stage-2.nix b/nixos/modules/system/boot/stage-2.nix
index 94bc34fea0d..f6b6a8e4b0b 100644
--- a/nixos/modules/system/boot/stage-2.nix
+++ b/nixos/modules/system/boot/stage-2.nix
@@ -10,7 +10,7 @@ let
     src = ./stage-2-init.sh;
     shellDebug = "${pkgs.bashInteractive}/bin/bash";
     shell = "${pkgs.bash}/bin/bash";
-    inherit (config.boot) systemdExecutable;
+    inherit (config.boot) systemdExecutable extraSystemdUnitPaths;
     isExecutable = true;
     inherit (config.nix) readOnlyStore;
     inherit useHostResolvConf;
@@ -20,6 +20,10 @@ let
       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}
@@ -82,6 +86,15 @@ in
           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.
+        '';
+      };
     };
 
   };
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index 6b672c7b2eb..d4ae4c93468 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -84,6 +84,7 @@ let
       # Kernel module loading.
       "systemd-modules-load.service"
       "kmod-static-nodes.service"
+      "modprobe@.service"
 
       # Filesystems.
       "systemd-fsck@.service"
@@ -175,8 +176,10 @@ let
       "timers.target.wants"
     ];
 
-  upstreamUserUnits =
-    [ "basic.target"
+    upstreamUserUnits = [
+      "app.slice"
+      "background.slice"
+      "basic.target"
       "bluetooth.target"
       "default.target"
       "exit.target"
@@ -184,6 +187,7 @@ let
       "graphical-session.target"
       "paths.target"
       "printer.target"
+      "session.slice"
       "shutdown.target"
       "smartcard.target"
       "sockets.target"
@@ -193,6 +197,7 @@ let
       "systemd-tmpfiles-clean.timer"
       "systemd-tmpfiles-setup.service"
       "timers.target"
+      "xdg-desktop-autostart.target"
     ];
 
   makeJobScript = name: text:
@@ -1183,9 +1188,12 @@ in
     systemd.services.systemd-remount-fs.unitConfig.ConditionVirtualization = "!container";
     systemd.services.systemd-random-seed.unitConfig.ConditionVirtualization = "!container";
 
-    boot.kernel.sysctl = mkIf (!cfg.coredump.enable) {
-      "kernel.core_pattern" = "core";
-    };
+    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";
   };
 
diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix
index 7478e3e8071..a450f303572 100644
--- a/nixos/modules/system/etc/etc.nix
+++ b/nixos/modules/system/etc/etc.nix
@@ -154,7 +154,7 @@ in
       ''
         # Set up the statically computed bits of /etc.
         echo "setting up /etc..."
-        ${pkgs.perl}/bin/perl -I${pkgs.perlPackages.FileSlurp}/${pkgs.perl.libPrefix} ${./setup-etc.pl} ${etc}/etc
+        ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc
       '';
 
   };
diff --git a/nixos/modules/tasks/cpu-freq.nix b/nixos/modules/tasks/cpu-freq.nix
index 513382936e4..f1219c07c50 100644
--- a/nixos/modules/tasks/cpu-freq.nix
+++ b/nixos/modules/tasks/cpu-freq.nix
@@ -19,7 +19,7 @@ in
       default = null;
       example = "ondemand";
       description = ''
-        Configure the governor used to regulate the frequence of the
+        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.
diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix
index a9b5b134d88..e468cb88003 100644
--- a/nixos/modules/tasks/filesystems.nix
+++ b/nixos/modules/tasks/filesystems.nix
@@ -22,8 +22,6 @@ let
                      # their assertions too
                      (attrValues config.fileSystems);
 
-  prioOption = prio: optionalString (prio != null) " pri=${toString prio}";
-
   specialFSTypes = [ "proc" "sysfs" "tmpfs" "ramfs" "devtmpfs" "devpts" ];
 
   coreFileSystemOpts = { name, config, ... }: {
@@ -240,6 +238,8 @@ in
         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: "defaults"
+          + optionalString (sw.priority != null) ",pri=${toString sw.priority}";
       in ''
         # This is a generated file.  Do not edit!
         #
@@ -262,7 +262,7 @@ in
 
         # Swap devices.
         ${flip concatMapStrings config.swapDevices (sw:
-            "${sw.realDevice} none swap${prioOption sw.priority}\n"
+            "${sw.realDevice} none swap ${swapOptions sw}\n"
         )}
       '';
 
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
index 59676e99678..21c30305188 100644
--- a/nixos/modules/tasks/filesystems/zfs.nix
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -302,7 +302,7 @@ in
     };
 
     services.zfs.autoScrub = {
-      enable = mkEnableOption "Enables periodic scrubbing of ZFS pools.";
+      enable = mkEnableOption "periodic scrubbing of ZFS pools";
 
       interval = mkOption {
         default = "Sun, 02:00";
@@ -446,7 +446,8 @@ in
         '') rootPools));
       };
 
-      boot.loader.grub = mkIf inInitrd {
+      # 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;
       };
 
diff --git a/nixos/modules/tasks/network-interfaces-scripted.nix b/nixos/modules/tasks/network-interfaces-scripted.nix
index 9ba6ccfbe71..11bd159319a 100644
--- a/nixos/modules/tasks/network-interfaces-scripted.nix
+++ b/nixos/modules/tasks/network-interfaces-scripted.nix
@@ -101,7 +101,7 @@ let
 
             unitConfig.ConditionCapability = "CAP_NET_ADMIN";
 
-            path = [ pkgs.iproute ];
+            path = [ pkgs.iproute2 ];
 
             serviceConfig = {
               Type = "oneshot";
@@ -185,7 +185,7 @@ let
             # Restart rather than stop+start this unit to prevent the
             # network from dying during switch-to-configuration.
             stopIfChanged = false;
-            path = [ pkgs.iproute ];
+            path = [ pkgs.iproute2 ];
             script =
               ''
                 state="/run/nixos/network/addresses/${i.name}"
@@ -258,7 +258,7 @@ let
             wantedBy = [ "network-setup.service" (subsystemDevice i.name) ];
             partOf = [ "network-setup.service" ];
             before = [ "network-setup.service" ];
-            path = [ pkgs.iproute ];
+            path = [ pkgs.iproute2 ];
             serviceConfig = {
               Type = "oneshot";
               RemainAfterExit = true;
@@ -284,7 +284,7 @@ let
             before = [ "network-setup.service" ];
             serviceConfig.Type = "oneshot";
             serviceConfig.RemainAfterExit = true;
-            path = [ pkgs.iproute ];
+            path = [ pkgs.iproute2 ];
             script = ''
               # Remove Dead Interfaces
               echo "Removing old bridge ${n}..."
@@ -372,7 +372,7 @@ let
             wants = deps; # if one or more interface fails, the switch should continue to run
             serviceConfig.Type = "oneshot";
             serviceConfig.RemainAfterExit = true;
-            path = [ pkgs.iproute config.virtualisation.vswitch.package ];
+            path = [ pkgs.iproute2 config.virtualisation.vswitch.package ];
             preStart = ''
               echo "Resetting Open vSwitch ${n}..."
               ovs-vsctl --if-exists del-br ${n} -- add-br ${n} \
@@ -413,7 +413,7 @@ let
             before = [ "network-setup.service" ];
             serviceConfig.Type = "oneshot";
             serviceConfig.RemainAfterExit = true;
-            path = [ pkgs.iproute pkgs.gawk ];
+            path = [ pkgs.iproute2 pkgs.gawk ];
             script = ''
               echo "Destroying old bond ${n}..."
               ${destroyBond n}
@@ -451,7 +451,7 @@ let
             before = [ "network-setup.service" ];
             serviceConfig.Type = "oneshot";
             serviceConfig.RemainAfterExit = true;
-            path = [ pkgs.iproute ];
+            path = [ pkgs.iproute2 ];
             script = ''
               # Remove Dead Interfaces
               ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
@@ -476,7 +476,7 @@ let
             before = [ "network-setup.service" ];
             serviceConfig.Type = "oneshot";
             serviceConfig.RemainAfterExit = true;
-            path = [ pkgs.iproute ];
+            path = [ pkgs.iproute2 ];
             script = ''
               # Remove Dead Interfaces
               ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
@@ -504,7 +504,7 @@ let
             before = [ "network-setup.service" ];
             serviceConfig.Type = "oneshot";
             serviceConfig.RemainAfterExit = true;
-            path = [ pkgs.iproute ];
+            path = [ pkgs.iproute2 ];
             script = ''
               # Remove Dead Interfaces
               ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
diff --git a/nixos/modules/tasks/network-interfaces-systemd.nix b/nixos/modules/tasks/network-interfaces-systemd.nix
index 23e1e611a71..1c145e8ff47 100644
--- a/nixos/modules/tasks/network-interfaces-systemd.nix
+++ b/nixos/modules/tasks/network-interfaces-systemd.nix
@@ -259,7 +259,7 @@ in
             wants = deps; # if one or more interface fails, the switch should continue to run
             serviceConfig.Type = "oneshot";
             serviceConfig.RemainAfterExit = true;
-            path = [ pkgs.iproute config.virtualisation.vswitch.package ];
+            path = [ pkgs.iproute2 config.virtualisation.vswitch.package ];
             preStart = ''
               echo "Resetting Open vSwitch ${n}..."
               ovs-vsctl --if-exists del-br ${n} -- add-br ${n} \
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index f730ec82bdf..3d1628d0783 100644
--- a/nixos/modules/tasks/network-interfaces.nix
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -144,33 +144,20 @@ let
       };
 
       tempAddress = mkOption {
-        type = types.enum [ "default" "enabled" "disabled" ];
-        default = if cfg.enableIPv6 then "default" else "disabled";
-        defaultText = literalExample ''if cfg.enableIPv6 then "default" else "disabled"'';
+        type = types.enum (lib.attrNames tempaddrValues);
+        default = cfg.tempAddresses;
+        defaultText = literalExample ''config.networking.tempAddresses'';
         description = ''
           When IPv6 is enabled with SLAAC, this option controls the use of
-          temporary address (aka privacy extensions). This is used to reduce tracking.
-          The three possible values are:
-
-          <itemizedlist>
-           <listitem>
-            <para>
-             <literal>"default"</literal> to generate temporary addresses and use
-             them by default;
-            </para>
-           </listitem>
-           <listitem>
-            <para>
-             <literal>"enabled"</literal> to generate temporary addresses but keep
-             using the standard EUI-64 ones by default;
-            </para>
-           </listitem>
-           <listitem>
-            <para>
-             <literal>"disabled"</literal> to completely disable temporary addresses.
-            </para>
-           </listitem>
-          </itemizedlist>
+          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}
         '';
       };
 
@@ -366,6 +353,32 @@ let
 
   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
 
 {
@@ -1039,6 +1052,21 @@ in
       '';
     };
 
+    networking.tempAddresses = mkOption {
+      default = if cfg.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}
+      '';
+    };
+
   };
 
 
@@ -1098,7 +1126,7 @@ in
       // listToAttrs (forEach interfaces
         (i: let
           opt = i.tempAddress;
-          val = { disabled = 0; enabled = 1; default = 2; }.${opt};
+          val = tempaddrValues.${opt}.sysctl;
          in nameValuePair "net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr" val));
 
     # Capabilities won't work unless we have at-least a 4.3 Linux
@@ -1144,7 +1172,7 @@ in
 
     environment.systemPackages =
       [ pkgs.host
-        pkgs.iproute
+        pkgs.iproute2
         pkgs.iputils
         pkgs.nettools
       ]
@@ -1171,7 +1199,7 @@ in
         wantedBy = [ "network.target" ];
         after = [ "network-pre.target" ];
         unitConfig.ConditionCapability = "CAP_NET_ADMIN";
-        path = [ pkgs.iproute ];
+        path = [ pkgs.iproute2 ];
         serviceConfig.Type = "oneshot";
         serviceConfig.RemainAfterExit = true;
         script = ''
@@ -1188,9 +1216,11 @@ in
       (pkgs.writeTextFile rec {
         name = "ipv6-privacy-extensions.rules";
         destination = "/etc/udev/rules.d/98-${name}";
-        text = ''
+        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 2 > /proc/sys/net/ipv6/conf/%k/use_tempaddr'"
+          ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.bash}/bin/sh -c 'echo ${sysctl-value} > /proc/sys/net/ipv6/conf/%k/use_tempaddr'"
         '';
       })
       (pkgs.writeTextFile rec {
@@ -1199,15 +1229,13 @@ in
         text = concatMapStrings (i:
           let
             opt = i.tempAddress;
-            val = if opt == "disabled" then 0 else 1;
-            msg = if opt == "disabled"
-                  then "completely disable IPv6 privacy addresses"
-                  else "enable IPv6 privacy addresses but prefer EUI-64 addresses";
+            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=${toString val}"
-          '') (filter (i: i.tempAddress != "default") interfaces);
+            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 {
@@ -1249,7 +1277,7 @@ in
               ${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.iproute}/bin/ip link set dev ${device} address ${current.mac}"}
+              ${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.
@@ -1260,7 +1288,7 @@ in
               ${optionalString (new.type == "mesh" && new.meshID!=null) "${pkgs.iw}/bin/iw dev ${device} set meshid ${new.meshID}"}
               ${optionalString (new.type == "monitor" && new.flags!=null) "${pkgs.iw}/bin/iw dev ${device} set monitor ${new.flags}"}
               ${optionalString (new.type == "managed" && new.fourAddr!=null) "${pkgs.iw}/bin/iw dev ${device} set 4addr ${if new.fourAddr then "on" else "off"}"}
-              ${optionalString (new.mac != null) "${pkgs.iproute}/bin/ip link set dev ${device} address ${new.mac}"}
+              ${optionalString (new.mac != null) "${pkgs.iproute2}/bin/ip link set dev ${device} address ${new.mac}"}
             '';
 
             # Udev attributes for systemd to name the device and to create a .device target.
diff --git a/nixos/modules/testing/service-runner.nix b/nixos/modules/testing/service-runner.nix
index 76e9d4a68c4..9060be3cca1 100644
--- a/nixos/modules/testing/service-runner.nix
+++ b/nixos/modules/testing/service-runner.nix
@@ -6,7 +6,7 @@ let
 
   makeScript = name: service: pkgs.writeScript "${name}-runner"
     ''
-      #! ${pkgs.perl}/bin/perl -w -I${pkgs.perlPackages.FileSlurp}/${pkgs.perl.libPrefix}
+      #! ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl -w
 
       use File::Slurp;
 
diff --git a/nixos/modules/virtualisation/amazon-init.nix b/nixos/modules/virtualisation/amazon-init.nix
index c5470b7af09..4f2f8df90eb 100644
--- a/nixos/modules/virtualisation/amazon-init.nix
+++ b/nixos/modules/virtualisation/amazon-init.nix
@@ -1,6 +1,10 @@
-{ config, pkgs, ... }:
+{ config, lib, pkgs, ... }:
+
+with lib;
 
 let
+  cfg = config.virtualisation.amazon-init;
+
   script = ''
     #!${pkgs.runtimeShell} -eu
 
@@ -12,6 +16,16 @@ let
 
     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
@@ -41,20 +55,33 @@ let
     nixos-rebuild switch
   '';
 in {
-  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" ];
+  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;
+      restartIfChanged = false;
+      unitConfig.X-StopOnRemoval = false;
 
-    serviceConfig = {
-      Type = "oneshot";
-      RemainAfterExit = true;
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
     };
   };
 }
diff --git a/nixos/modules/virtualisation/anbox.nix b/nixos/modules/virtualisation/anbox.nix
index da5df358073..7b096bd1a9f 100644
--- a/nixos/modules/virtualisation/anbox.nix
+++ b/nixos/modules/virtualisation/anbox.nix
@@ -98,7 +98,6 @@ in
       environment.XDG_RUNTIME_DIR="${anboxloc}";
 
       wantedBy = [ "multi-user.target" ];
-      after = [ "systemd-udev-settle.service" ];
       preStart = let
         initsh = pkgs.writeText "nixos-init" (''
           #!/system/bin/sh
diff --git a/nixos/modules/virtualisation/azure-image.nix b/nixos/modules/virtualisation/azure-image.nix
index 60fed3222ef..03dd3c05130 100644
--- a/nixos/modules/virtualisation/azure-image.nix
+++ b/nixos/modules/virtualisation/azure-image.nix
@@ -9,8 +9,9 @@ in
 
   options = {
     virtualisation.azureImage.diskSize = mkOption {
-      type = with types; int;
-      default = 2048;
+      type = with types; either (enum [ "auto" ]) int;
+      default = "auto";
+      example = 2048;
       description = ''
         Size of disk image. Unit is MB.
       '';
diff --git a/nixos/modules/virtualisation/brightbox-image.nix b/nixos/modules/virtualisation/brightbox-image.nix
index 4498e3a7361..9641b693f18 100644
--- a/nixos/modules/virtualisation/brightbox-image.nix
+++ b/nixos/modules/virtualisation/brightbox-image.nix
@@ -119,7 +119,7 @@ in
       wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
 
-      path = [ pkgs.wget pkgs.iproute ];
+      path = [ pkgs.wget pkgs.iproute2 ];
 
       script =
         ''
diff --git a/nixos/modules/virtualisation/containerd.nix b/nixos/modules/virtualisation/containerd.nix
new file mode 100644
index 00000000000..194276d1695
--- /dev/null
+++ b/nixos/modules/virtualisation/containerd.nix
@@ -0,0 +1,60 @@
+{ pkgs, lib, config, ... }:
+let
+  cfg = config.virtualisation.containerd;
+  containerdConfigChecked = pkgs.runCommand "containerd-config-checked.toml" { nativeBuildInputs = [pkgs.containerd]; } ''
+    containerd -c ${cfg.configFile} config dump >/dev/null
+    ln -s ${cfg.configFile} $out
+  '';
+in
+{
+
+  options.virtualisation.containerd = with lib.types; {
+    enable = lib.mkEnableOption "containerd container runtime";
+
+    configFile = lib.mkOption {
+      default = null;
+      description = "path to containerd config file";
+      type = nullOr path;
+    };
+
+    args = lib.mkOption {
+      default = {};
+      description = "extra args to append to the containerd cmdline";
+      type = attrsOf str;
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    virtualisation.containerd.args.config = lib.mkIf (cfg.configFile != null) (toString containerdConfigChecked);
+
+    environment.systemPackages = [pkgs.containerd];
+
+    systemd.services.containerd = {
+      description = "containerd - container runtime";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = with pkgs; [
+        containerd
+        runc
+        iptables
+      ];
+      serviceConfig = {
+        ExecStart = ''${pkgs.containerd}/bin/containerd ${lib.concatStringsSep " " (lib.cli.toGNUCommandLine {} cfg.args)}'';
+        Delegate = "yes";
+        KillMode = "process";
+        Type = "notify";
+        Restart = "always";
+        RestartSec = "5";
+        StartLimitBurst = "8";
+        StartLimitIntervalSec = "120s";
+
+        # "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";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/containers.nix b/nixos/modules/virtualisation/containers.nix
index 997edf77ba9..3974caf2233 100644
--- a/nixos/modules/virtualisation/containers.nix
+++ b/nixos/modules/virtualisation/containers.nix
@@ -4,15 +4,7 @@ let
 
   inherit (lib) mkOption types;
 
-  # Once https://github.com/NixOS/nixpkgs/pull/75584 is merged we can use the TOML generator
-  toTOML = name: value: pkgs.runCommandNoCC name {
-    nativeBuildInputs = [ pkgs.remarshal ];
-    value = builtins.toJSON value;
-    passAsFile = [ "value" ];
-  } ''
-    json2toml "$valuePath" "$out"
-  '';
-
+  toml = pkgs.formats.toml { };
 in
 {
   meta = {
@@ -26,6 +18,11 @@ in
       [ "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 = {
@@ -45,23 +42,10 @@ in
       description = "Enable the OCI seccomp BPF hook";
     };
 
-    containersConf = mkOption {
-      default = {};
+    containersConf.settings = mkOption {
+      type = toml.type;
+      default = { };
       description = "containers.conf configuration";
-      type = types.submodule {
-        options = {
-
-          extraConfig = mkOption {
-            type = types.lines;
-            default = "";
-            description = ''
-              Extra configuration that should be put in the containers.conf
-              configuration file
-            '';
-
-          };
-        };
-      };
     };
 
     registries = {
@@ -113,20 +97,19 @@ in
   };
 
   config = lib.mkIf cfg.enable {
+    virtualisation.containers.containersConf.settings = {
+      network.cni_plugin_dirs = [ "${pkgs.cni-plugins}/bin/" ];
+      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".text = ''
-      [network]
-      cni_plugin_dirs = ["${pkgs.cni-plugins}/bin/"]
-
-      ${lib.optionalString (cfg.ociSeccompBpfHook.enable == true) ''
-      [engine]
-      hooks_dir = [
-        "${config.boot.kernelPackages.oci-seccomp-bpf-hook}",
-      ]
-      ''}
-    '' + cfg.containersConf.extraConfig;
+    environment.etc."containers/containers.conf".source =
+      toml.generate "containers.conf" cfg.containersConf.settings;
 
-    environment.etc."containers/registries.conf".source = toTOML "registries.conf" {
+    environment.etc."containers/registries.conf".source = toml.generate "registries.conf" {
       registries = lib.mapAttrs (n: v: { registries = v; }) cfg.registries;
     };
 
diff --git a/nixos/modules/virtualisation/digital-ocean-image.nix b/nixos/modules/virtualisation/digital-ocean-image.nix
index b582e235d43..0ff2ee591f2 100644
--- a/nixos/modules/virtualisation/digital-ocean-image.nix
+++ b/nixos/modules/virtualisation/digital-ocean-image.nix
@@ -10,8 +10,9 @@ in
 
   options = {
     virtualisation.digitalOceanImage.diskSize = mkOption {
-      type = with types; int;
-      default = 4096;
+      type = with types; either (enum [ "auto" ]) int;
+      default = "auto";
+      example = 4096;
       description = ''
         Size of disk image. Unit is MB.
       '';
diff --git a/nixos/modules/virtualisation/docker.nix b/nixos/modules/virtualisation/docker.nix
index b1415bf021d..3eb0de3a855 100644
--- a/nixos/modules/virtualisation/docker.nix
+++ b/nixos/modules/virtualisation/docker.nix
@@ -157,6 +157,7 @@ in
 
       systemd.services.docker = {
         wantedBy = optional cfg.enableOnBoot "multi-user.target";
+        after = [ "network.target" "docker.socket" ];
         requires = [ "docker.socket" ];
         environment = proxy_env;
         serviceConfig = {
diff --git a/nixos/modules/virtualisation/ec2-data.nix b/nixos/modules/virtualisation/ec2-data.nix
index 62912535018..1b764e7e4d8 100644
--- a/nixos/modules/virtualisation/ec2-data.nix
+++ b/nixos/modules/virtualisation/ec2-data.nix
@@ -19,7 +19,7 @@ with lib;
         wantedBy = [ "multi-user.target" "sshd.service" ];
         before = [ "sshd.service" ];
 
-        path = [ pkgs.iproute ];
+        path = [ pkgs.iproute2 ];
 
         script =
           ''
diff --git a/nixos/modules/virtualisation/ec2-metadata-fetcher.nix b/nixos/modules/virtualisation/ec2-metadata-fetcher.nix
index dca5c2abd4e..760f024f33f 100644
--- a/nixos/modules/virtualisation/ec2-metadata-fetcher.nix
+++ b/nixos/modules/virtualisation/ec2-metadata-fetcher.nix
@@ -71,7 +71,7 @@
   }
 
   wget_imds -O "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
-  wget_imds -O "$metaDir/user-data" http://169.254.169.254/1.0/user-data && chmod 600 "$metaDir/user-data"
+  (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/gce-images.nix b/nixos/modules/virtualisation/gce-images.nix
index 5354d91deb9..7b027619a44 100644
--- a/nixos/modules/virtualisation/gce-images.nix
+++ b/nixos/modules/virtualisation/gce-images.nix
@@ -5,5 +5,13 @@ let self = {
   "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";
-  latest = self."18.09";
+
+  # 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
index b6b1ffa3958..cff48d20b2b 100644
--- a/nixos/modules/virtualisation/google-compute-config.nix
+++ b/nixos/modules/virtualisation/google-compute-config.nix
@@ -110,7 +110,7 @@ in
   systemd.services.google-network-daemon = {
     description = "Google Compute Engine Network Daemon";
     after = [ "network-online.target" "network.target" "google-instance-setup.service" ];
-    path = with pkgs; [ iproute ];
+    path = with pkgs; [ iproute2 ];
     serviceConfig = {
       ExecStart = "${gce}/bin/google_network_daemon";
       StandardOutput="journal+console";
diff --git a/nixos/modules/virtualisation/google-compute-image.nix b/nixos/modules/virtualisation/google-compute-image.nix
index e2332df611a..79c3921669e 100644
--- a/nixos/modules/virtualisation/google-compute-image.nix
+++ b/nixos/modules/virtualisation/google-compute-image.nix
@@ -18,8 +18,9 @@ in
 
   options = {
     virtualisation.googleComputeImage.diskSize = mkOption {
-      type = with types; int;
-      default = 1536;
+      type = with types; either (enum [ "auto" ]) int;
+      default = "auto";
+      example = 1536;
       description = ''
         Size of disk image. Unit is MB.
       '';
diff --git a/nixos/modules/virtualisation/hyperv-guest.nix b/nixos/modules/virtualisation/hyperv-guest.nix
index a3656c307f9..b3bcfff1980 100644
--- a/nixos/modules/virtualisation/hyperv-guest.nix
+++ b/nixos/modules/virtualisation/hyperv-guest.nix
@@ -56,6 +56,8 @@ in {
     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
index fabc9113dfc..6845d675009 100644
--- a/nixos/modules/virtualisation/hyperv-image.nix
+++ b/nixos/modules/virtualisation/hyperv-image.nix
@@ -9,8 +9,9 @@ in {
   options = {
     hyperv = {
       baseImageSize = mkOption {
-        type = types.int;
-        default = 2048;
+        type = with types; either (enum [ "auto" ]) int;
+        default = "auto";
+        example = 2048;
         description = ''
           The size of the hyper-v base image in MiB.
         '';
diff --git a/nixos/modules/virtualisation/libvirtd.nix b/nixos/modules/virtualisation/libvirtd.nix
index 1d6a9457dde..f43c44f5dca 100644
--- a/nixos/modules/virtualisation/libvirtd.nix
+++ b/nixos/modules/virtualisation/libvirtd.nix
@@ -46,6 +46,15 @@ in {
       '';
     };
 
+    package = mkOption {
+      type = types.package;
+      default = pkgs.libvirt;
+      defaultText = "pkgs.libvirt";
+      description = ''
+        libvirt package to use.
+      '';
+    };
+
     qemuPackage = mkOption {
       type = types.package;
       default = pkgs.qemu;
@@ -149,7 +158,7 @@ in {
       # 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; [ libvirt libressl.nc iptables cfg.qemuPackage ];
+      systemPackages = with pkgs; [ libressl.nc iptables cfg.package cfg.qemuPackage ];
       etc.ethertypes.source = "${pkgs.iptables}/etc/ethertypes";
     };
 
@@ -169,26 +178,26 @@ in {
       source = "/run/${dirName}/nix-helpers/qemu-bridge-helper";
     };
 
-    systemd.packages = [ pkgs.libvirt ];
+    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 ${pkgs.libvirt}/var/lib && echo \
+        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 ${pkgs.libvirt}/var/lib/$i /var/lib/$i
+            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 ${pkgs.libvirt}/libexec/libvirt_lxc ${cfg.qemuPackage}/bin/qemu-kvm ${cfg.qemuPackage}/bin/qemu-system-*; do
+        for emulator in ${cfg.package}/libexec/libvirt_lxc ${cfg.qemuPackage}/bin/qemu-kvm ${cfg.qemuPackage}/bin/qemu-system-*; do
           ln -s --force "$emulator" /run/${dirName}/nix-emulators/
         done
 
@@ -213,7 +222,7 @@ in {
 
     systemd.services.libvirtd = {
       requires = [ "libvirtd-config.service" ];
-      after = [ "systemd-udev-settle.service" "libvirtd-config.service" ]
+      after = [ "libvirtd-config.service" ]
               ++ optional vswitch.enable "ovs-vswitchd.service";
 
       environment.LIBVIRTD_ARGS = escapeShellArgs (
@@ -234,7 +243,7 @@ in {
 
     systemd.services.libvirt-guests = {
       wantedBy = [ "multi-user.target" ];
-      path = with pkgs; [ coreutils libvirt gawk ];
+      path = with pkgs; [ coreutils gawk cfg.package ];
       restartIfChanged = false;
 
       environment.ON_BOOT = "${cfg.onBoot}";
@@ -249,7 +258,7 @@ in {
 
     systemd.services.virtlogd = {
       description = "Virtual machine log manager";
-      serviceConfig.ExecStart = "@${pkgs.libvirt}/sbin/virtlogd virtlogd";
+      serviceConfig.ExecStart = "@${cfg.package}/sbin/virtlogd virtlogd";
       restartIfChanged = false;
     };
 
@@ -261,7 +270,7 @@ in {
 
     systemd.services.virtlockd = {
       description = "Virtual machine lock manager";
-      serviceConfig.ExecStart = "@${pkgs.libvirt}/sbin/virtlockd virtlockd";
+      serviceConfig.ExecStart = "@${cfg.package}/sbin/virtlockd virtlockd";
       restartIfChanged = false;
     };
 
diff --git a/nixos/modules/virtualisation/lxd.nix b/nixos/modules/virtualisation/lxd.nix
index 4b2adf4cc69..96e8d68ae50 100644
--- a/nixos/modules/virtualisation/lxd.nix
+++ b/nixos/modules/virtualisation/lxd.nix
@@ -66,7 +66,7 @@ in {
         type = types.bool;
         default = false;
         description = ''
-          enables various settings to avoid common pitfalls when
+          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!".
@@ -74,6 +74,17 @@ in {
           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.
+        '';
+      };
     };
   };
 
@@ -81,40 +92,58 @@ in {
   config = mkIf cfg.enable {
     environment.systemPackages = [ cfg.package ];
 
-    security.apparmor = {
-      enable = true;
-      profiles = [
-        "${cfg.lxcPackage}/etc/apparmor.d/usr.bin.lxc-start"
-        "${cfg.lxcPackage}/etc/apparmor.d/lxc-containers"
-      ];
-      packages = [ cfg.lxcPackage ];
-    };
+    # 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 ];
+    security.apparmor.profiles = [
+      "${cfg.lxcPackage}/etc/apparmor.d/lxc-containers"
+      "${cfg.lxcPackage}/etc/apparmor.d/usr.bin.lxc-start"
+    ];
 
     # 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 = [ "systemd-udev-settle.service" ];
+      after = [ "network-online.target" "lxcfs.service" ];
+      requires = [ "network-online.target" "lxd.socket"  "lxcfs.service" ];
+      documentation = [ "man:lxd(1)" ];
 
-      path = lib.optional config.boot.zfs.enabled config.boot.zfs.package;
-
-      preStart = ''
-        mkdir -m 0755 -p /var/lib/lxc/rootfs
-      '';
+      path = optional cfg.zfsSupport config.boot.zfs.package;
 
       serviceConfig = {
         ExecStart = "@${cfg.package}/bin/lxd lxd --group lxd";
-        Type = "simple";
+        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
diff --git a/nixos/modules/virtualisation/nixos-containers.nix b/nixos/modules/virtualisation/nixos-containers.nix
index 3754fe6dac6..a158509a77a 100644
--- a/nixos/modules/virtualisation/nixos-containers.nix
+++ b/nixos/modules/virtualisation/nixos-containers.nix
@@ -35,6 +35,9 @@ let
       ''
         #! ${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" ] ||
@@ -60,8 +63,12 @@ let
 
         ${concatStringsSep "\n" (mapAttrsToList renderExtraVeth cfg.extraVeths)}
 
-        # Start the regular stage 1 script.
-        exec "$1"
+        # 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"
       ''
     );
 
@@ -127,12 +134,16 @@ let
       ''}
 
       # Run systemd-nspawn without startup notification (we'll
-      # wait for the container systemd to signal readiness).
+      # 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 \
@@ -259,13 +270,10 @@ let
     Slice = "machine.slice";
     Delegate = true;
 
-    # Hack: we don't want to kill systemd-nspawn, since we call
-    # "machinectl poweroff" in preStop to shut down the
-    # container cleanly. But systemd requires sending a signal
-    # (at least if we want remaining processes to be killed
-    # after the timeout). So send an ignored signal.
+    # We rely on systemd-nspawn turning a SIGTERM to itself into a shutdown
+    # signal (SIGRTMIN+3) for the inner container.
     KillMode = "mixed";
-    KillSignal = "WINCH";
+    KillSignal = "TERM";
 
     DevicePolicy = "closed";
     DeviceAllow = map (d: "${d.node} ${d.modifier}") cfg.allowedDevices;
@@ -439,21 +447,16 @@ in
       default = false;
       description = ''
         Whether this NixOS machine is a lightweight container running
-        in another NixOS system. If set to true, support for nested
-        containers is disabled by default, but can be reenabled by
-        setting <option>boot.enableContainers</option> to true.
+        in another NixOS system.
       '';
     };
 
     boot.enableContainers = mkOption {
       type = types.bool;
-      default = !config.boot.isContainer;
+      default = true;
       description = ''
         Whether to enable support for NixOS containers. Defaults to true
-        (at no cost if containers are not actually used), but only if the
-        system is not itself a lightweight container of a host.
-        To enable support for nested containers, this option has to be
-        explicitly set to true (in the outer container).
+        (at no cost if containers are not actually used).
       '';
     };
 
@@ -504,7 +507,7 @@ in
 
             path = mkOption {
               type = types.path;
-              example = "/nix/var/nix/profiles/containers/webserver";
+              example = "/nix/var/nix/profiles/per-container/webserver";
               description = ''
                 As an alternative to specifying
                 <option>config</option>, you can specify the path to
@@ -739,7 +742,7 @@ in
 
       unitConfig.RequiresMountsFor = "/var/lib/containers/%i";
 
-      path = [ pkgs.iproute ];
+      path = [ pkgs.iproute2 ];
 
       environment = {
         root = "/var/lib/containers/%i";
@@ -752,8 +755,6 @@ in
 
       postStart = postStartScript dummyConfig;
 
-      preStop = "machinectl poweroff $INSTANCE";
-
       restartIfChanged = false;
 
       serviceConfig = serviceDirectives dummyConfig;
diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix
index 2dd15e3aba4..ad436ed3014 100644
--- a/nixos/modules/virtualisation/oci-containers.nix
+++ b/nixos/modules/virtualisation/oci-containers.nix
@@ -59,6 +59,18 @@ let
         '';
         };
 
+        environmentFiles = mkOption {
+          type = with types; listOf path;
+          default = [];
+          description = "Environment files for this container.";
+          example = literalExample ''
+            [
+              /path/to/.env
+              /path/to/.env.secret
+            ]
+        '';
+        };
+
         log-driver = mkOption {
           type = types.str;
           default = "journald";
@@ -236,6 +248,7 @@ let
     ] ++ 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
diff --git a/nixos/modules/virtualisation/openstack-metadata-fetcher.nix b/nixos/modules/virtualisation/openstack-metadata-fetcher.nix
index 8c191397cf9..133cd4c0e9f 100644
--- a/nixos/modules/virtualisation/openstack-metadata-fetcher.nix
+++ b/nixos/modules/virtualisation/openstack-metadata-fetcher.nix
@@ -15,7 +15,7 @@
   }
 
   wget_imds -O "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
-  wget_imds -O "$metaDir/user-data" http://169.254.169.254/1.0/user-data && chmod 600 "$metaDir/user-data"
+  (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/podman.nix b/nixos/modules/virtualisation/podman.nix
index 0223c0df1f2..d6421d488b8 100644
--- a/nixos/modules/virtualisation/podman.nix
+++ b/nixos/modules/virtualisation/podman.nix
@@ -96,13 +96,12 @@ in
 
       virtualisation.containers = {
         enable = true; # Enable common /etc/containers configuration
-        containersConf.extraConfig = lib.optionalString cfg.enableNvidia
-          (builtins.readFile (toml.generate "podman.nvidia.containers.conf" {
-            engine = {
-              conmon_env_vars = [ "PATH=${lib.makeBinPath [ pkgs.nvidia-podman ]}" ];
-              runtimes.nvidia = [ "${pkgs.nvidia-podman}/bin/nvidia-container-runtime" ];
-            };
-          }));
+        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 ];
diff --git a/nixos/modules/virtualisation/virtualbox-image.nix b/nixos/modules/virtualisation/virtualbox-image.nix
index fa580e8b42d..071edda8269 100644
--- a/nixos/modules/virtualisation/virtualbox-image.nix
+++ b/nixos/modules/virtualisation/virtualbox-image.nix
@@ -11,8 +11,9 @@ in {
   options = {
     virtualbox = {
       baseImageSize = mkOption {
-        type = types.int;
-        default = 50 * 1024;
+        type = with types; either (enum [ "auto" ]) int;
+        default = "auto";
+        example = 50 * 1024;
         description = ''
           The size of the VirtualBox base image in MiB.
         '';
diff --git a/nixos/modules/virtualisation/vmware-image.nix b/nixos/modules/virtualisation/vmware-image.nix
index 9da9e145f7a..f6cd12e2bb7 100644
--- a/nixos/modules/virtualisation/vmware-image.nix
+++ b/nixos/modules/virtualisation/vmware-image.nix
@@ -18,8 +18,9 @@ in {
   options = {
     vmware = {
       baseImageSize = mkOption {
-        type = types.int;
-        default = 2048;
+        type = with types; either (enum [ "auto" ]) int;
+        default = "auto";
+        example = 2048;
         description = ''
           The size of the VMWare base image in MiB.
         '';
diff --git a/nixos/modules/virtualisation/xe-guest-utilities.nix b/nixos/modules/virtualisation/xe-guest-utilities.nix
index 675cf929737..25ccbaebc07 100644
--- a/nixos/modules/virtualisation/xe-guest-utilities.nix
+++ b/nixos/modules/virtualisation/xe-guest-utilities.nix
@@ -17,7 +17,7 @@ in {
       wantedBy    = [ "multi-user.target" ];
       after = [ "xe-linux-distribution.service" ];
       requires = [ "proc-xen.mount" ];
-      path = [ pkgs.coreutils pkgs.iproute ];
+      path = [ pkgs.coreutils pkgs.iproute2 ];
       serviceConfig = {
         PIDFile = "/run/xe-daemon.pid";
         ExecStart = "${pkgs.xe-guest-utilities}/bin/xe-daemon -p /run/xe-daemon.pid";
diff --git a/nixos/modules/virtualisation/xen-dom0.nix b/nixos/modules/virtualisation/xen-dom0.nix
index 5b57ca860da..fea43727f2f 100644
--- a/nixos/modules/virtualisation/xen-dom0.nix
+++ b/nixos/modules/virtualisation/xen-dom0.nix
@@ -161,9 +161,6 @@ in
 
     environment.systemPackages = [ cfg.package ];
 
-    # Make sure Domain 0 gets the required configuration
-    #boot.kernelPackages = pkgs.boot.kernelPackages.override { features={xen_dom0=true;}; };
-
     boot.kernelModules =
       [ "xen-evtchn" "xen-gntdev" "xen-gntalloc" "xen-blkback" "xen-netback"
         "xen-pciback" "evtchn" "gntdev" "netbk" "blkbk" "xen-scsibk"
@@ -248,7 +245,7 @@ in
     # Xen provides udev rules.
     services.udev.packages = [ cfg.package ];
 
-    services.udev.path = [ pkgs.bridge-utils pkgs.iproute ];
+    services.udev.path = [ pkgs.bridge-utils pkgs.iproute2 ];
 
     systemd.services.xen-store = {
       description = "Xen Store Daemon";
diff --git a/nixos/release.nix b/nixos/release.nix
index 327a259de7f..746e4c9dc69 100644
--- a/nixos/release.nix
+++ b/nixos/release.nix
@@ -138,7 +138,7 @@ in rec {
   # Build the initial ramdisk so Hydra can keep track of its size over time.
   initialRamdisk = buildFromConfig ({ ... }: { }) (config: config.system.build.initialRamdisk);
 
-  netboot = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system: makeNetboot {
+  netboot = forMatchingSystems supportedSystems (system: makeNetboot {
     module = ./modules/installer/netboot/netboot-minimal.nix;
     inherit system;
   });
@@ -224,6 +224,25 @@ in rec {
   );
 
 
+  # 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)
+
+  );
+
+
   # 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 {
diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix
index c6d393d9196..99dd8ec6fd3 100644
--- a/nixos/tests/acme.nix
+++ b/nixos/tests/acme.nix
@@ -253,7 +253,7 @@ in import ./make-test-python.nix ({ lib, ... }: {
 
 
       def check_connection(node, domain, retries=3):
-          assert retries >= 0
+          assert retries >= 0, f"Failed to connect to https://{domain}"
 
           result = node.succeed(
               "openssl s_client -brief -verify 2 -CAfile /tmp/ca.crt"
@@ -262,12 +262,12 @@ in import ./make-test-python.nix ({ lib, ... }: {
 
           for line in result.lower().split("\n"):
               if "verification" in line and "error" in line:
-                  time.sleep(1)
+                  time.sleep(3)
                   return check_connection(node, domain, retries - 1)
 
 
       def check_connection_key_bits(node, domain, bits, retries=3):
-          assert retries >= 0
+          assert retries >= 0, f"Did not find expected number of bits ({bits}) in key"
 
           result = node.succeed(
               "openssl s_client -CAfile /tmp/ca.crt"
@@ -277,12 +277,12 @@ in import ./make-test-python.nix ({ lib, ... }: {
           print("Key type:", result)
 
           if bits not in result:
-              time.sleep(1)
+              time.sleep(3)
               return check_connection_key_bits(node, domain, bits, retries - 1)
 
 
       def check_stapling(node, domain, retries=3):
-          assert retries >= 0
+          assert retries >= 0, "OCSP Stapling check failed"
 
           # Pebble doesn't provide a full OCSP responder, so just check the URL
           result = node.succeed(
@@ -293,10 +293,23 @@ in import ./make-test-python.nix ({ lib, ... }: {
           print("OCSP Responder URL:", result)
 
           if "${caDomain}:4002" not in result.lower():
-              time.sleep(1)
+              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)
+
+
       client.start()
       dnsserver.start()
 
@@ -313,8 +326,7 @@ in import ./make-test-python.nix ({ lib, ... }: {
       acme.wait_for_unit("network-online.target")
       acme.wait_for_unit("pebble.service")
 
-      client.succeed("curl https://${caDomain}:15000/roots/0 > /tmp/ca.crt")
-      client.succeed("curl https://${caDomain}:15000/intermediate-keys/0 >> /tmp/ca.crt")
+      download_ca_certs(client)
 
       with subtest("Can request certificate with HTTPS-01 challenge"):
           webserver.wait_for_unit("acme-finished-a.example.test.target")
@@ -322,6 +334,21 @@ in import ./make-test-python.nix ({ lib, ... }: {
           check_issuer(webserver, "a.example.test", "pebble")
           check_connection(client, "a.example.test")
 
+      with subtest("Certificates and accounts have safe + valid permissions"):
+          group = "${nodes.webserver.config.security.acme.certs."a.example.test".group}"
+          webserver.succeed(
+              f"test $(stat -L -c \"%a %U %G\" /var/lib/acme/a.example.test/* | 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/**/* | tee /dev/stderr | grep '640 acme {group}' | wc -l) -eq 5"
+          )
+          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"
+          )
+
       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")
@@ -375,8 +402,15 @@ in import ./make-test-python.nix ({ lib, ... }: {
           assert keyhash_old == keyhash_new
 
       with subtest("Can request certificates for vhost + aliases (apache-httpd)"):
-          switch_to(webserver, "httpd-aliases")
-          webserver.wait_for_unit("acme-finished-c.example.test.target")
+          try:
+              switch_to(webserver, "httpd-aliases")
+              webserver.wait_for_unit("acme-finished-c.example.test.target")
+          except Exception as err:
+              _, output = webserver.execute(
+                  "cat /var/log/httpd/*.log && ls -al /var/lib/acme/acme-challenge"
+              )
+              print(output)
+              raise err
           check_issuer(webserver, "c.example.test", "pebble")
           check_connection(client, "c.example.test")
           check_connection(client, "d.example.test")
diff --git a/nixos/tests/agda.nix b/nixos/tests/agda.nix
index 3773907cff5..f282788519c 100644
--- a/nixos/tests/agda.nix
+++ b/nixos/tests/agda.nix
@@ -3,8 +3,9 @@ import ./make-test-python.nix ({ pkgs, ... }:
 let
   hello-world = pkgs.writeText "hello-world" ''
     open import IO
+    open import Level
 
-    main = run(putStrLn "Hello World!")
+    main = run {0â„“} (putStrLn "Hello World!")
   '';
 in
 {
diff --git a/nixos/tests/airsonic.nix b/nixos/tests/airsonic.nix
new file mode 100644
index 00000000000..59bd84877c6
--- /dev/null
+++ b/nixos/tests/airsonic.nix
@@ -0,0 +1,32 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "airsonic";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ sumnerevans ];
+  };
+
+  machine =
+    { pkgs, ... }:
+    {
+      services.airsonic = {
+        enable = true;
+        maxMemory = 800;
+      };
+
+      # Airsonic is a Java application, and unfortunately requires a significant
+      # amount of memory.
+      virtualisation.memorySize = 1024;
+    };
+
+  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
index 773f91d8604..c34bf3623b6 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -24,6 +24,8 @@ in
   _3proxy = handleTest ./3proxy.nix {};
   acme = handleTest ./acme.nix {};
   agda = handleTest ./agda.nix {};
+  airsonic = handleTest ./airsonic.nix {};
+  amazon-init-shell = handleTest ./amazon-init-shell.nix {};
   ammonite = handleTest ./ammonite.nix {};
   atd = handleTest ./atd.nix {};
   avahi = handleTest ./avahi.nix {};
@@ -47,8 +49,9 @@ in
   buildkite-agents = handleTest ./buildkite-agents.nix {};
   caddy = handleTest ./caddy.nix {};
   cadvisor = handleTestOn ["x86_64-linux"] ./cadvisor.nix {};
-  cage = handleTest ./cage.nix {};
+  cage = handleTestOn ["x86_64-linux"] ./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; };
@@ -74,6 +77,7 @@ in
   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 {};
@@ -84,8 +88,10 @@ in
   couchdb = handleTest ./couchdb.nix {};
   cri-o = handleTestOn ["x86_64-linux"] ./cri-o.nix {};
   custom-ca = handleTest ./custom-ca.nix {};
+  croc = handleTest ./croc.nix {};
   deluge = handleTest ./deluge.nix {};
   dhparams = handleTest ./dhparams.nix {};
+  discourse = handleTest ./discourse.nix {};
   dnscrypt-proxy2 = handleTestOn ["x86_64-linux"] ./dnscrypt-proxy2.nix {};
   dnscrypt-wrapper = handleTestOn ["x86_64-linux"] ./dnscrypt-wrapper {};
   doas = handleTest ./doas.nix {};
@@ -109,6 +115,7 @@ in
   ergo = handleTest ./ergo.nix {};
   etcd = handleTestOn ["x86_64-linux"] ./etcd.nix {};
   etcd-cluster = handleTestOn ["x86_64-linux"] ./etcd-cluster.nix {};
+  etebase-server = handleTest ./etebase-server.nix {};
   etesync-dav = handleTest ./etesync-dav.nix {};
   fancontrol = handleTest ./fancontrol.nix {};
   fcitx = handleTest ./fcitx {};
@@ -134,6 +141,7 @@ in
   gnome3 = handleTest ./gnome3.nix {};
   gnome3-xorg = handleTest ./gnome3-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 {};
@@ -175,10 +183,12 @@ in
   initrd-network-ssh = handleTest ./initrd-network-ssh {};
   initrdNetwork = handleTest ./initrd-network.nix {};
   initrd-secrets = handleTest ./initrd-secrets.nix {};
+  inspircd = handleTest ./inspircd.nix {};
   installer = handleTest ./installer.nix {};
   iodine = handleTest ./iodine.nix {};
   ipfs = handleTest ./ipfs.nix {};
   ipv6 = handleTest ./ipv6.nix {};
+  iscsi-root = handleTest ./iscsi-root.nix {};
   jackett = handleTest ./jackett.nix {};
   jellyfin = handleTest ./jellyfin.nix {};
   jenkins = handleTest ./jenkins.nix {};
@@ -189,9 +199,7 @@ in
   keepalived = handleTest ./keepalived.nix {};
   keepassxc = handleTest ./keepassxc.nix {};
   kerberos = handleTest ./kerberos/default.nix {};
-  kernel-latest = handleTest ./kernel-latest.nix {};
-  kernel-lts = handleTest ./kernel-lts.nix {};
-  kernel-testing = handleTest ./kernel-testing.nix {};
+  kernel-generic = handleTest ./kernel-generic.nix {};
   kernel-latest-ath-user-regd = handleTest ./kernel-latest-ath-user-regd.nix {};
   keycloak = discoverTests (import ./keycloak.nix);
   keymap = handleTest ./keymap.nix {};
@@ -223,6 +231,7 @@ in
   mariadb-galera-mariabackup = handleTest ./mysql/mariadb-galera-mariabackup.nix {};
   mariadb-galera-rsync = handleTest ./mysql/mariadb-galera-rsync.nix {};
   matomo = handleTest ./matomo.nix {};
+  matrix-appservice-irc = handleTest ./matrix-appservice-irc.nix {};
   matrix-synapse = handleTest ./matrix-synapse.nix {};
   mediawiki = handleTest ./mediawiki.nix {};
   memcached = handleTest ./memcached.nix {};
@@ -257,6 +266,7 @@ in
   nat.standalone = handleTest ./nat.nix { withFirewall = false; };
   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; };
@@ -285,6 +295,7 @@ in
   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 {};
   opensmtpd = handleTest ./opensmtpd.nix {};
@@ -307,11 +318,17 @@ in
   pgjwt = handleTest ./pgjwt.nix {};
   pgmanage = handleTest ./pgmanage.nix {};
   php = handleTest ./php {};
+  php73 = handleTest ./php { php = pkgs.php73; };
+  php74 = handleTest ./php { php = pkgs.php74; };
+  php80 = handleTest ./php { php = pkgs.php80; };
   pinnwand = handleTest ./pinnwand.nix {};
   plasma5 = handleTest ./plasma5.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.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 {};
   postgis = handleTest ./postgis.nix {};
@@ -322,6 +339,7 @@ in
   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 {};
@@ -329,7 +347,6 @@ in
   proxy = handleTest ./proxy.nix {};
   pt2-clone = handleTest ./pt2-clone.nix {};
   qboot = handleTestOn ["x86_64-linux" "i686-linux"] ./qboot.nix {};
-  quagga = handleTest ./quagga.nix {};
   quorum = handleTest ./quorum.nix {};
   rabbitmq = handleTest ./rabbitmq.nix {};
   radarr = handleTest ./radarr.nix {};
@@ -370,6 +387,7 @@ in
   sssd-ldap = handleTestOn ["x86_64-linux"] ./sssd-ldap.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 {};
@@ -387,6 +405,7 @@ in
   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 {};
   telegraf = handleTest ./telegraf.nix {};
   tiddlywiki = handleTest ./tiddlywiki.nix {};
@@ -397,11 +416,13 @@ in
   # traefik test relies on docker-containers
   trac = handleTest ./trac.nix {};
   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 {};
   tuptime = handleTest ./tuptime.nix {};
+  turbovnc-headless-server = handleTest ./turbovnc-headless-server.nix {};
   ucg = handleTest ./ucg.nix {};
   udisks2 = handleTest ./udisks2.nix {};
   unbound = handleTest ./unbound.nix {};
@@ -417,7 +438,9 @@ in
   virtualbox = handleTestOn ["x86_64-linux"] ./virtualbox.nix {};
   vscodium = handleTest ./vscodium.nix {};
   wasabibackend = handleTest ./wasabibackend.nix {};
+  wiki-js = handleTest ./wiki-js.nix {};
   wireguard = handleTest ./wireguard {};
+  wmderland = handleTest ./wmderland.nix {};
   wordpress = handleTest ./wordpress.nix {};
   xandikos = handleTest ./xandikos.nix {};
   xautolock = handleTest ./xautolock.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/babeld.nix b/nixos/tests/babeld.nix
index 5817ea4ce14..d4df6f86d08 100644
--- a/nixos/tests/babeld.nix
+++ b/nixos/tests/babeld.nix
@@ -25,9 +25,6 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
       {
         virtualisation.vlans = [ 10 20 ];
 
-        boot.kernel.sysctl."net.ipv4.conf.all.forwarding" = 1;
-        boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
-
         networking = {
           useDHCP = false;
           firewall.enable = false;
@@ -74,9 +71,6 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
       {
         virtualisation.vlans = [ 20 30 ];
 
-        boot.kernel.sysctl."net.ipv4.conf.all.forwarding" = 1;
-        boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
-
         networking = {
           useDHCP = false;
           firewall.enable = false;
diff --git a/nixos/tests/bitwarden.nix b/nixos/tests/bitwarden.nix
index 5345c7245d0..3813a1f70f9 100644
--- a/nixos/tests/bitwarden.nix
+++ b/nixos/tests/bitwarden.nix
@@ -113,6 +113,7 @@ let
                   driver.find_element_by_css_selector('input#masterPasswordRetype').send_keys(
                     '${userPassword}'
                   )
+                  driver.find_element_by_css_selector('input#acceptPolicies').click()
 
                   driver.find_element_by_xpath("//button[contains(., 'Submit')]").click()
 
diff --git a/nixos/tests/cage.nix b/nixos/tests/cage.nix
index 1ae07b6fd2f..80ce1e0d8b3 100644
--- a/nixos/tests/cage.nix
+++ b/nixos/tests/cage.nix
@@ -3,7 +3,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
 {
   name = "cage";
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ matthewbauer flokli ];
+    maintainers = [ matthewbauer ];
   };
 
   machine = { ... }:
@@ -13,19 +13,15 @@ import ./make-test-python.nix ({ pkgs, ...} :
     services.cage = {
       enable = true;
       user = "alice";
-      program = "${pkgs.xterm}/bin/xterm -cm -pc"; # disable color and bold to make OCR easier
+      # Disable color and bold and use a larger font to make OCR easier:
+      program = "${pkgs.xterm}/bin/xterm -cm -pc -fa Monospace -fs 24";
     };
 
-    # this needs a fairly recent kernel, otherwise:
-    #   [backend/drm/util.c:215] Unable to add DRM framebuffer: No such file or directory
-    #   [backend/drm/legacy.c:15] Virtual-1: Failed to set CRTC: No such file or directory
-    #   [backend/drm/util.c:215] Unable to add DRM framebuffer: No such file or directory
-    #   [backend/drm/legacy.c:15] Virtual-1: Failed to set CRTC: No such file or directory
-    #   [backend/drm/drm.c:618] Failed to initialize renderer on connector 'Virtual-1': initial page-flip failed
-    #   [backend/drm/drm.c:701] Failed to initialize renderer for plane
-    boot.kernelPackages = pkgs.linuxPackages_latest;
-
     virtualisation.memorySize = 1024;
+    # Need to switch to a different VGA card / GPU driver because Cage segfaults with the default one (std):
+    # machine # [   14.355893] .cage-wrapped[736]: segfault at 20 ip 00007f035fa0d8c7 sp 00007ffce9e4a2f0 error 4 in libwlroots.so.8[7f035fa07000+5a000]
+    # machine # [   14.358108] Code: 4f a8 ff ff eb aa 0f 1f 44 00 00 c3 0f 1f 80 00 00 00 00 41 54 49 89 f4 55 31 ed 53 48 89 fb 48 8d 7f 18 48 8d 83 b8 00 00 00 <80> 7f 08 00 75 0d 48 83 3f 00 0f 85 91 00 00 00 48 89 fd 48 83 c7
+    virtualisation.qemu.options = [ "-vga virtio" ];
   };
 
   enableOCR = true;
diff --git a/nixos/tests/calibre-web.nix b/nixos/tests/calibre-web.nix
new file mode 100644
index 00000000000..4f73b331112
--- /dev/null
+++ b/nixos/tests/calibre-web.nix
@@ -0,0 +1,53 @@
+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 = {
+          default = { ... }: {
+            services.calibre-web.enable = true;
+          };
+
+          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()
+
+          default.wait_for_unit("calibre-web.service")
+          default.wait_for_open_port(${toString defaultPort})
+          default.succeed(
+              "curl --fail 'http://localhost:${toString defaultPort}/basicconfig' | grep -q 'Basic Configuration'"
+          )
+
+          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 -q test-book"
+          )
+        '';
+      }
+)
diff --git a/nixos/tests/ceph-multi-node.nix b/nixos/tests/ceph-multi-node.nix
index 4e6d644f96c..33736e27b98 100644
--- a/nixos/tests/ceph-multi-node.nix
+++ b/nixos/tests/ceph-multi-node.nix
@@ -37,7 +37,7 @@ let
 
   generateHost = { pkgs, cephConfig, networkConfig, ... }: {
     virtualisation = {
-      memorySize = 512;
+      memorySize = 1024;
       emptyDiskImages = [ 20480 ];
       vlans = [ 1 ];
     };
@@ -120,6 +120,7 @@ let
     )
     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'")
diff --git a/nixos/tests/ceph-single-node-bluestore.nix b/nixos/tests/ceph-single-node-bluestore.nix
index cc873e8aee5..f706d4d56fc 100644
--- a/nixos/tests/ceph-single-node-bluestore.nix
+++ b/nixos/tests/ceph-single-node-bluestore.nix
@@ -34,7 +34,7 @@ let
 
   generateHost = { pkgs, cephConfig, networkConfig, ... }: {
     virtualisation = {
-      memorySize = 512;
+      memorySize = 1024;
       emptyDiskImages = [ 20480 20480 20480 ];
       vlans = [ 1 ];
     };
@@ -95,6 +95,7 @@ let
     )
     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'")
diff --git a/nixos/tests/ceph-single-node.nix b/nixos/tests/ceph-single-node.nix
index 19919371a3c..d1d56ea6708 100644
--- a/nixos/tests/ceph-single-node.nix
+++ b/nixos/tests/ceph-single-node.nix
@@ -34,7 +34,7 @@ let
 
   generateHost = { pkgs, cephConfig, networkConfig, ... }: {
     virtualisation = {
-      memorySize = 512;
+      memorySize = 1024;
       emptyDiskImages = [ 20480 20480 20480 ];
       vlans = [ 1 ];
     };
@@ -95,6 +95,7 @@ let
     )
     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'")
diff --git a/nixos/tests/containers-imperative.nix b/nixos/tests/containers-imperative.nix
index 0ff0d3f9545..bb207165a02 100644
--- a/nixos/tests/containers-imperative.nix
+++ b/nixos/tests/containers-imperative.nix
@@ -111,6 +111,26 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
           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} &")
+          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(
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/couchdb.nix b/nixos/tests/couchdb.nix
index d038ee7d890..049532481b1 100644
--- a/nixos/tests/couchdb.nix
+++ b/nixos/tests/couchdb.nix
@@ -24,8 +24,6 @@ with lib;
   };
 
   nodes = {
-    couchdb1 = makeNode pkgs.couchdb testuser testpass;
-    couchdb2 = makeNode pkgs.couchdb2 testuser testpass;
     couchdb3 = makeNode pkgs.couchdb3 testuser testpass;
   };
 
@@ -41,42 +39,6 @@ with lib;
   in ''
     start_all()
 
-    couchdb1.wait_for_unit("couchdb.service")
-    couchdb1.wait_until_succeeds(
-        "${curlJqCheck "" "GET" "" ".couchdb" "Welcome"}"
-    )
-    couchdb1.wait_until_succeeds(
-        "${curlJqCheck "" "GET" "_all_dbs" ". | length" "2"}"
-    )
-    couchdb1.succeed("${curlJqCheck testlogin "PUT" "foo" ".ok" "true"}")
-    couchdb1.succeed(
-        "${curlJqCheck "" "GET" "_all_dbs" ". | length" "3"}"
-    )
-    couchdb1.succeed(
-        "${curlJqCheck testlogin "DELETE" "foo" ".ok" "true"}"
-    )
-    couchdb1.succeed(
-        "${curlJqCheck "" "GET" "_all_dbs" ". | length" "2"}"
-    )
-
-    couchdb2.wait_for_unit("couchdb.service")
-    couchdb2.wait_until_succeeds(
-        "${curlJqCheck "" "GET" "" ".couchdb" "Welcome"}"
-    )
-    couchdb2.wait_until_succeeds(
-        "${curlJqCheck "" "GET" "_all_dbs" ". | length" "0"}"
-    )
-    couchdb2.succeed("${curlJqCheck testlogin "PUT" "foo" ".ok" "true"}")
-    couchdb2.succeed(
-        "${curlJqCheck "" "GET" "_all_dbs" ". | length" "1"}"
-    )
-    couchdb2.succeed(
-        "${curlJqCheck testlogin "DELETE" "foo" ".ok" "true"}"
-    )
-    couchdb2.succeed(
-        "${curlJqCheck "" "GET" "_all_dbs" ". | length" "0"}"
-    )
-
     couchdb3.wait_for_unit("couchdb.service")
     couchdb3.wait_until_succeeds(
         "${curlJqCheck testlogin "GET" "" ".couchdb" "Welcome"}"
diff --git a/nixos/tests/croc.nix b/nixos/tests/croc.nix
new file mode 100644
index 00000000000..75a8fc991d4
--- /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 &"
+    )
+
+    # 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/custom-ca.nix b/nixos/tests/custom-ca.nix
index 67f7b3ff1f1..31909188d3a 100644
--- a/nixos/tests/custom-ca.nix
+++ b/nixos/tests/custom-ca.nix
@@ -92,13 +92,19 @@ in
         { onlySSL = true;
           sslCertificate = "${example-good-cert}/server.crt";
           sslCertificateKey = "${example-good-cert}/server.key";
-          locations."/".extraConfig = "return 200 'It works!';";
+          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 = "return 200 'It does not work!';";
+          locations."/".extraConfig = ''
+            add_header Content-Type text/plain;
+            return 200 'It does not work!';
+          '';
         };
 
       environment.systemPackages = with pkgs;
diff --git a/nixos/tests/discourse.nix b/nixos/tests/discourse.nix
new file mode 100644
index 00000000000..3c965550fe0
--- /dev/null
+++ b/nixos/tests/discourse.nix
@@ -0,0 +1,197 @@
+# 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, ... }:
+  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;
+
+        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.discourse = {
+          enable = true;
+          inherit admin;
+          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/docker-tools.nix b/nixos/tests/docker-tools.nix
index 6638ec4927c..96662b4540c 100644
--- a/nixos/tests/docker-tools.nix
+++ b/nixos/tests/docker-tools.nix
@@ -161,12 +161,18 @@ import ./make-test-python.nix ({ pkgs, ... }: {
             "docker run --rm ${examples.layered-image.imageName} cat extraCommands",
         )
 
-    with subtest("Ensure building an image on top of a layered Docker images work"):
+    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(
@@ -205,6 +211,31 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         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}'"
@@ -219,6 +250,18 @@ import ./make-test-python.nix ({ pkgs, ... }: {
             "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 correct behavior when no store is needed"):
         # This check tests that buildLayeredImage can build images that don't need a store.
         docker.succeed(
@@ -254,5 +297,72 @@ import ./make-test-python.nix ({ pkgs, ... }: {
             "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$'"
+        )
   '';
 })
diff --git a/nixos/tests/doh-proxy-rust.nix b/nixos/tests/doh-proxy-rust.nix
new file mode 100644
index 00000000000..ca150cafab5
--- /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 '{url}?dns={query}' | grep -qF {bin_ip}")
+  '';
+})
diff --git a/nixos/tests/dokuwiki.nix b/nixos/tests/dokuwiki.nix
index 40475d789d4..2664e1500ea 100644
--- a/nixos/tests/dokuwiki.nix
+++ b/nixos/tests/dokuwiki.nix
@@ -9,7 +9,7 @@ let
       sha256 = "4de5ff31d54dd61bbccaf092c9e74c1af3a4c53e07aa59f60457a8f00cfb23a6";
     };
     # We need unzip to build this package
-    buildInputs = [ pkgs.unzip ];
+    nativeBuildInputs = [ pkgs.unzip ];
     # Installing simply means copying all files to the output directory
     installPhase = "mkdir -p $out; cp -R * $out/";
   };
@@ -24,7 +24,7 @@ let
       sha256 = "e40ed7dd6bbe7fe3363bbbecb4de481d5e42385b5a0f62f6a6ce6bf3a1f9dfa8";
     };
     # We need unzip to build this package
-    buildInputs = [ pkgs.unzip ];
+    nativeBuildInputs = [ pkgs.unzip ];
     sourceRoot = ".";
     # Installing simply means copying all files to the output directory
     installPhase = "mkdir -p $out; cp -R * $out/";
diff --git a/nixos/tests/dovecot.nix b/nixos/tests/dovecot.nix
index 1129e3b45d9..8913c2a6a7e 100644
--- a/nixos/tests/dovecot.nix
+++ b/nixos/tests/dovecot.nix
@@ -8,6 +8,8 @@ import ./make-test-python.nix {
       enable = true;
       protocols = [ "imap" "pop3" ];
       modules = [ pkgs.dovecot_pigeonhole ];
+      mailUser = "vmail";
+      mailGroup = "vmail";
     };
     environment.systemPackages = let
       sendTestMail = pkgs.writeScriptBin "send-testmail" ''
diff --git a/nixos/tests/elk.nix b/nixos/tests/elk.nix
index 8488c97c01e..fee350de65b 100644
--- a/nixos/tests/elk.nix
+++ b/nixos/tests/elk.nix
@@ -120,6 +120,7 @@ let
           };
       };
 
+    passthru.elkPackages = elk;
     testScript = ''
       import json
 
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/fancontrol.nix b/nixos/tests/fancontrol.nix
index 356cd57ffa1..296c6802641 100644
--- a/nixos/tests/fancontrol.nix
+++ b/nixos/tests/fancontrol.nix
@@ -1,28 +1,34 @@
 import ./make-test-python.nix ({ pkgs, ... } : {
   name = "fancontrol";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ evils ];
+  };
 
-  machine =
-    { ... }:
-    { 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
-      '';
+  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()
-    machine.wait_for_unit("fancontrol.service")
-    machine.wait_until_succeeds(
-        "journalctl -eu fancontrol | grep 'Configuration appears to be outdated'"
+    # 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/gitdaemon.nix b/nixos/tests/gitdaemon.nix
index d0156fb9a49..bb07b6e97b7 100644
--- a/nixos/tests/gitdaemon.nix
+++ b/nixos/tests/gitdaemon.nix
@@ -18,6 +18,11 @@ in {
 
         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";
@@ -35,7 +40,6 @@ in {
 
     with subtest("create project.git"):
         server.succeed(
-            "mkdir /git",
             "git init --bare /git/project.git",
             "touch /git/project.git/git-daemon-export-ok",
         )
diff --git a/nixos/tests/gitea.nix b/nixos/tests/gitea.nix
index 1fb27593f05..037fc7b31bf 100644
--- a/nixos/tests/gitea.nix
+++ b/nixos/tests/gitea.nix
@@ -61,7 +61,7 @@ let
           + "Please contact your site administrator.'"
       )
       server.succeed(
-          "su -l gitea -c 'GITEA_WORK_DIR=/var/lib/gitea gitea admin create-user "
+          "su -l gitea -c 'GITEA_WORK_DIR=/var/lib/gitea gitea admin user create "
           + "--username test --password totallysafe --email test@localhost'"
       )
 
diff --git a/nixos/tests/gitlab.nix b/nixos/tests/gitlab.nix
index ba085338944..af2ab12bf4b 100644
--- a/nixos/tests/gitlab.nix
+++ b/nixos/tests/gitlab.nix
@@ -11,6 +11,8 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
 
   nodes = {
     gitlab = { ... }: {
+      imports = [ common/user-account.nix ];
+
       virtualisation.memorySize = if pkgs.stdenv.is64bit then 4096 else 2047;
       systemd.services.gitlab.serviceConfig.Restart = mkForce "no";
       systemd.services.gitlab-workhorse.serviceConfig.Restart = mkForce "no";
@@ -27,71 +29,126 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : with lib; {
         };
       };
 
+      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;
+          };
+          pages = {
+            enabled = true;
+            host = "localhost";
+          };
+        };
         secrets = {
-          secretFile = pkgs.writeText "secret" "r8X9keSKynU7p4aKlh4GO1Bo77g5a7vj";
-          otpFile = pkgs.writeText "otpsecret" "Zu5hGx3YvQx40DvI8WoZJQpX2paSDOlG";
-          dbFile = pkgs.writeText "dbsecret" "lsGltKWTejOf6JxCVa7nLDenzkO9wPLR";
+          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 =
-  let
-    auth = pkgs.writeText "auth.json" (builtins.toJSON {
-      grant_type = "password";
-      username = "root";
-      password = initialRootPassword;
-    });
+  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")
+        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")
+      '';
 
-    createProject = pkgs.writeText "create-project.json" (builtins.toJSON {
-      name = "test";
-    });
+      # 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 -q 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")
+      '';
 
-    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";
-    });
-  in
-  ''
-    gitlab.start()
-    gitlab.wait_for_unit("gitaly.service")
-    gitlab.wait_for_unit("gitlab-workhorse.service")
-    gitlab.wait_for_unit("gitlab.service")
-    gitlab.wait_for_unit("gitlab-sidekiq.service")
-    gitlab.wait_for_file("/var/gitlab/state/tmp/sockets/gitlab.socket")
-    gitlab.wait_until_succeeds("curl -sSf http://gitlab/users/sign_in")
-    gitlab.succeed(
-        "curl -isSf http://gitlab | grep -i location | grep -q 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"
-    )
-    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 gitlab-postgresql.service")
+      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/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/hibernate.nix b/nixos/tests/hibernate.nix
index 8251c6e7ef8..ae506c8542f 100644
--- a/nixos/tests/hibernate.nix
+++ b/nixos/tests/hibernate.nix
@@ -1,44 +1,120 @@
 # Test whether hibernation from partition works.
 
-import ./make-test-python.nix (pkgs: {
-  name = "hibernate";
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+}:
 
-  nodes = {
-    machine = { config, lib, pkgs, ... }: with lib; {
-      virtualisation.emptyDiskImages = [ config.virtualisation.memorySize ];
+with import ../lib/testing-python.nix { inherit system pkgs; };
 
-      systemd.services.backdoor.conflicts = [ "sleep.target" ];
+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
+    ];
 
-      swapDevices = mkOverride 0 [ { device = "/dev/vdb"; } ];
+    hardware.enableAllFirmware = mkForce false;
+    documentation.nixos.enable = false;
+    boot.loader.grub.device = "/dev/vda";
 
-      networking.firewall.allowedTCPPorts = [ 4444 ];
+    systemd.services.backdoor.conflicts = [ "sleep.target" ];
 
-      systemd.services.listener.serviceConfig.ExecStart = "${pkgs.netcat}/bin/nc -l 4444 -k";
+    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
+      ];
 
-    probe = { pkgs, ...}: {
-      environment.systemPackages = [ pkgs.netcat ];
+      nix.binaryCaches = mkForce [ ];
+      nix.extraOptions = ''
+        hashed-mirrors =
+        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 work around it we run a daemon which listens to a TCP connection and
-  # try to connect to it as a test.
+  # To avoid this, we install NixOS onto a temporary disk with everything we need
+  # included into the store.
 
   testScript =
     ''
+      def create_named_machine(name):
+          return 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,
+              }
+          )
+
+
+      # Install NixOS
       machine.start()
-      machine.wait_for_unit("multi-user.target")
-      machine.succeed("mkswap /dev/vdb")
-      machine.succeed("swapon -a")
-      machine.start_job("listener")
-      machine.wait_for_open_port(4444)
-      machine.succeed("systemctl hibernate &")
-      machine.wait_for_shutdown()
-      probe.wait_for_unit("multi-user.target")
-      machine.start()
-      probe.wait_until_succeeds("echo test | nc machine 4444 -N")
+      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",
+      )
+      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.succeed("systemctl hibernate &")
+      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/hledger-web.nix b/nixos/tests/hledger-web.nix
index 378d819437d..f8919f7d4bd 100644
--- a/nixos/tests/hledger-web.nix
+++ b/nixos/tests/hledger-web.nix
@@ -13,25 +13,22 @@ rec {
   name = "hledger-web";
   meta.maintainers = with lib.maintainers; [ marijanp ];
 
-  nodes = {
-    server = { config, pkgs, ... }: rec {
+  nodes = rec {
+    server = { config, pkgs, ... }: {
       services.hledger-web = {
         host = "127.0.0.1";
         port = 5000;
         enable = true;
-        journalFile = journal;
+        capabilities.manage = true;
       };
-      networking.firewall.allowedTCPPorts = [ services.hledger-web.port ];
+      networking.firewall.allowedTCPPorts = [ config.services.hledger-web.port ];
+      systemd.services.hledger-web.preStart = ''
+        ln -s ${journal} /var/lib/hledger-web/.hledger.journal
+      '';
     };
-    apiserver = { config, pkgs, ... }: rec {
-      services.hledger-web = {
-        host = "127.0.0.1";
-        port = 5000;
-        enable = true;
-        serveApi = true;
-        journalFile = journal;
-      };
-      networking.firewall.allowedTCPPorts = [ services.hledger-web.port ];
+    apiserver = { ... }: {
+      imports = [ server ];
+      services.hledger-web.serveApi = true;
     };
   };
 
@@ -42,7 +39,7 @@ rec {
     server.wait_for_open_port(5000)
     with subtest("Check if web UI is accessible"):
         page = server.succeed("curl -L http://127.0.0.1:5000")
-        assert "test.journal" in page
+        assert ".hledger.journal" in page
 
     apiserver.wait_for_unit("hledger-web.service")
     apiserver.wait_for_open_port(5000)
diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix
index 131f50747fe..c75dd248ecb 100644
--- a/nixos/tests/home-assistant.nix
+++ b/nixos/tests/home-assistant.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ pkgs, ... }:
+import ./make-test-python.nix ({ pkgs, lib, ... }:
 
 let
   configDir = "/var/lib/foobar";
@@ -6,17 +6,16 @@ let
   mqttPassword = "secret";
 in {
   name = "home-assistant";
-  meta = with pkgs.lib; {
-    maintainers = with maintainers; [ dotlambda ];
-  };
+  meta.maintainers = lib.teams.home-assistant.members;
 
   nodes.hass = { pkgs, ... }: {
     environment.systemPackages = with pkgs; [ mosquitto ];
     services.mosquitto = {
       enable = true;
+      checkPasswords = true;
       users = {
         "${mqttUsername}" = {
-          acl = [ "pattern readwrite #" ];
+          acl = [ "topic readwrite #" ];
           password = mqttPassword;
         };
       };
@@ -24,6 +23,8 @@ in {
     services.home-assistant = {
       inherit configDir;
       enable = true;
+      # includes the package with all tests enabled
+      package = pkgs.home-assistant;
       config = {
         homeassistant = {
           name = "Home";
@@ -44,6 +45,10 @@ in {
           payload_on = "let_there_be_light";
           payload_off = "off";
         }];
+        emulated_hue = {
+          host_ip = "127.0.0.1";
+          listen_port = 80;
+        };
         logger = {
           default = "info";
           logs."homeassistant.components.mqtt" = "debug";
@@ -75,13 +80,13 @@ in {
         hass.wait_for_open_port(8123)
         hass.succeed("curl --fail http://localhost:8123/lovelace")
     with subtest("Toggle a binary sensor using MQTT"):
-        # wait for broker to become available
-        hass.wait_until_succeeds(
-            "mosquitto_sub -V mqttv311 -t home-assistant/test -u ${mqttUsername} -P '${mqttPassword}' -W 1 -t '*'"
-        )
+        hass.wait_for_open_port(1883)
         hass.succeed(
-            "mosquitto_pub -V mqttv311 -t home-assistant/test -u ${mqttUsername} -P '${mqttPassword}' -m let_there_be_light"
+            "mosquitto_pub -V mqttv5 -t home-assistant/test -u ${mqttUsername} -P '${mqttPassword}' -m let_there_be_light"
         )
+    with subtest("Check that capabilities are passed for emulated_hue to bind to port 80"):
+        hass.wait_for_open_port(80)
+        hass.succeed("curl --fail http://localhost:80/description.xml")
     with subtest("Print log to ease debugging"):
         output_log = hass.succeed("cat ${configDir}/home-assistant.log")
         print("\n### home-assistant.log ###\n")
@@ -93,5 +98,8 @@ in {
     # example line: 2020-06-20 10:01:32 DEBUG (MainThread) [homeassistant.components.mqtt] Received message on home-assistant/test: b'let_there_be_light'
     with subtest("Check we received the mosquitto message"):
         assert "let_there_be_light" in output_log
+
+    with subtest("Check systemd unit hardening"):
+        hass.log(hass.succeed("systemd-analyze security home-assistant.service"))
   '';
 })
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/default.nix b/nixos/tests/installed-tests/default.nix
index e5d7009bb7b..6c2846a1636 100644
--- a/nixos/tests/installed-tests/default.nix
+++ b/nixos/tests/installed-tests/default.nix
@@ -97,6 +97,7 @@ in
   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 {};
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/pipewire.nix b/nixos/tests/installed-tests/pipewire.nix
index f4154b5d2fd..b04265658fc 100644
--- a/nixos/tests/installed-tests/pipewire.nix
+++ b/nixos/tests/installed-tests/pipewire.nix
@@ -2,4 +2,14 @@
 
 makeInstalledTest {
   tested = pkgs.pipewire;
+  testConfig = {
+    hardware.pulseaudio.enable = false;
+    services.pipewire = {
+      enable = true;
+      pulse.enable = true;
+      jack.enable = true;
+      alsa.enable = true;
+      alsa.support32Bit = true;
+    };
+  };
 }
diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix
index 904ec17229e..48f0f593425 100644
--- a/nixos/tests/installer.nix
+++ b/nixos/tests/installer.nix
@@ -75,7 +75,7 @@ let
     else ''
       def assemble_qemu_flags():
           flags = "-cpu max"
-          ${if system == "x86_64-linux"
+          ${if (system == "x86_64-linux" || system == "i686-linux")
             then ''flags += " -m 1024"''
             else ''flags += " -m 768 -enable-kvm -machine virt,gic-version=host"''
           }
@@ -294,7 +294,7 @@ let
           # the same during and after installation.
           virtualisation.emptyDiskImages = [ 512 ];
           virtualisation.bootDevice =
-            if grubVersion == 1 then "/dev/sdb" else "/dev/vdb";
+            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";
 
@@ -695,22 +695,23 @@ in {
   };
 
   # Test a basic install using GRUB 1.
-  grub1 = makeInstallerTest "grub1" {
+  grub1 = makeInstallerTest "grub1" rec {
     createPartitions = ''
       machine.succeed(
-          "flock /dev/sda parted --script /dev/sda -- mklabel msdos"
+          "flock ${grubDevice} parted --script ${grubDevice} -- mklabel msdos"
           + " mkpart primary linux-swap 1M 1024M"
           + " mkpart primary ext2 1024M -1s",
           "udevadm settle",
-          "mkswap /dev/sda1 -L swap",
+          "mkswap ${grubDevice}-part1 -L swap",
           "swapon -L swap",
-          "mkfs.ext3 -L nixos /dev/sda2",
+          "mkfs.ext3 -L nixos ${grubDevice}-part2",
           "mount LABEL=nixos /mnt",
           "mkdir -p /mnt/tmp",
       )
     '';
     grubVersion = 1;
-    grubDevice = "/dev/sda";
+    # /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
diff --git a/nixos/tests/ipv6.nix b/nixos/tests/ipv6.nix
index f9d6d82b54a..75faa6f6020 100644
--- a/nixos/tests/ipv6.nix
+++ b/nixos/tests/ipv6.nix
@@ -8,12 +8,34 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
   };
 
   nodes =
-    # Remove the interface configuration provided by makeTest so that the
-    # interfaces are all configured implicitly
-    { client = { ... }: { networking.interfaces = lib.mkForce {}; };
+    {
+      # 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 ];
@@ -40,9 +62,12 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
       # 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()
 
-      client.wait_for_unit("network.target")
+      for client in clients:
+          client.wait_for_unit("network.target")
       server.wait_for_unit("network.target")
       server.wait_for_unit("httpd.service")
 
@@ -64,28 +89,42 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
 
 
       with subtest("Loopback address can be pinged"):
-          client.succeed("ping -c 1 ::1 >&2")
-          client.fail("ping -c 1 ::2 >&2")
+          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"):
-          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")
+          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"):
-          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 can be obtained and pinged"):
-          ip = wait_for_address(client, "eth1", "global", temporary=True)
+          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.succeed(f"ip r g ::2 | grep {ip}")
-
-      # TODO: test reachability of a machine on another network.
+          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-root.nix b/nixos/tests/iscsi-root.nix
new file mode 100644
index 00000000000..bda51d2c2e4
--- /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.binaryCaches = lib.mkForce [];
+            nix.extraOptions = ''
+              hashed-mirrors =
+              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/jellyfin.nix b/nixos/tests/jellyfin.nix
index 65360624d48..f8c2429a7b8 100644
--- a/nixos/tests/jellyfin.nix
+++ b/nixos/tests/jellyfin.nix
@@ -1,16 +1,156 @@
-import ./make-test-python.nix ({ lib, ...}:
-
-{
-  name = "jellyfin";
-  meta.maintainers = with lib.maintainers; [ minijackson ];
-
-  machine =
-    { ... }:
-    { services.jellyfin.enable = true; };
-
-  testScript = ''
-    machine.wait_for_unit("jellyfin.service")
-    machine.wait_for_open_port(8096)
-    machine.succeed("curl --fail http://localhost:8096/")
-  '';
-})
+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
+        import time
+        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(f"/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}"
+                + f" /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/kafka.nix b/nixos/tests/kafka.nix
index d5c54f7d991..034601c815b 100644
--- a/nixos/tests/kafka.nix
+++ b/nixos/tests/kafka.nix
@@ -30,11 +30,6 @@ let
           '';
           package = kafkaPackage;
           zookeeper = "zookeeper1:2181";
-          # These are the default options, but UseCompressedOops doesn't work with 32bit JVM
-          jvmOptions = [
-            "-server" "-Xmx1G" "-Xms1G" "-XX:+UseParNewGC" "-XX:+UseConcMarkSweepGC" "-XX:+CMSClassUnloadingEnabled"
-            "-XX:+CMSScavengeBeforeRemark" "-XX:+DisableExplicitGC" "-Djava.awt.headless=true" "-Djava.net.preferIPv4Stack=true"
-          ] ++ optionals (! pkgs.stdenv.isi686 ) [ "-XX:+UseCompressedOops" ];
         };
 
         networking.firewall.allowedTCPPorts = [ 9092 ];
@@ -82,4 +77,5 @@ let
 in with pkgs; {
   kafka_2_4  = makeKafkaTest "kafka_2_4"  apacheKafka_2_4;
   kafka_2_5  = makeKafkaTest "kafka_2_5"  apacheKafka_2_5;
+  kafka_2_6  = makeKafkaTest "kafka_2_6"  apacheKafka_2_6;
 }
diff --git a/nixos/tests/kernel-generic.nix b/nixos/tests/kernel-generic.nix
new file mode 100644
index 00000000000..17089141e9e
--- /dev/null
+++ b/nixos/tests/kernel-generic.nix
@@ -0,0 +1,37 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}@args:
+
+with pkgs.lib;
+
+let
+  makeKernelTest = version: linuxPackages: (import ./make-test-python.nix ({ pkgs, ... }: {
+    name = "kernel-${version}";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ nequissimus ];
+    };
+
+    machine = { ... }:
+      {
+        boot.kernelPackages = linuxPackages;
+      };
+
+    testScript =
+      ''
+        assert "Linux" in machine.succeed("uname -s")
+        assert "${linuxPackages.kernel.modDirVersion}" in machine.succeed("uname -a")
+      '';
+  }) args);
+in
+with pkgs; {
+  linux_4_4 = makeKernelTest "4.4" linuxPackages_4_4;
+  linux_4_9 = makeKernelTest "4.9" linuxPackages_4_9;
+  linux_4_14 = makeKernelTest "4.14" linuxPackages_4_14;
+  linux_4_19 = makeKernelTest "4.19" linuxPackages_4_19;
+  linux_5_4 = makeKernelTest "5.4" linuxPackages_5_4;
+  linux_5_10 = makeKernelTest "5.10" linuxPackages_5_10;
+  linux_5_11 = makeKernelTest "5.11" linuxPackages_5_11;
+
+  linux_testing = makeKernelTest "testing" linuxPackages_testing;
+}
diff --git a/nixos/tests/kernel-latest.nix b/nixos/tests/kernel-latest.nix
deleted file mode 100644
index 323dde267a4..00000000000
--- a/nixos/tests/kernel-latest.nix
+++ /dev/null
@@ -1,17 +0,0 @@
-import ./make-test-python.nix ({ pkgs, ...} : {
-  name = "kernel-latest";
-  meta = with pkgs.lib.maintainers; {
-    maintainers = [ nequissimus ];
-  };
-
-  machine = { pkgs, ... }:
-    {
-      boot.kernelPackages = pkgs.linuxPackages_latest;
-    };
-
-  testScript =
-    ''
-      assert "Linux" in machine.succeed("uname -s")
-      assert "${pkgs.linuxPackages_latest.kernel.version}" in machine.succeed("uname -a")
-    '';
-})
diff --git a/nixos/tests/kernel-lts.nix b/nixos/tests/kernel-lts.nix
deleted file mode 100644
index 9b03e9db6d8..00000000000
--- a/nixos/tests/kernel-lts.nix
+++ /dev/null
@@ -1,17 +0,0 @@
-import ./make-test-python.nix ({ pkgs, ...} : {
-  name = "kernel-lts";
-  meta = with pkgs.lib.maintainers; {
-    maintainers = [ nequissimus ];
-  };
-
-  machine = { pkgs, ... }:
-    {
-      boot.kernelPackages = pkgs.linuxPackages;
-    };
-
-  testScript =
-    ''
-      assert "Linux" in machine.succeed("uname -s")
-      assert "${pkgs.linuxPackages.kernel.version}" in machine.succeed("uname -a")
-    '';
-})
diff --git a/nixos/tests/kernel-testing.nix b/nixos/tests/kernel-testing.nix
deleted file mode 100644
index 017007c0aec..00000000000
--- a/nixos/tests/kernel-testing.nix
+++ /dev/null
@@ -1,17 +0,0 @@
-import ./make-test-python.nix ({ pkgs, ...} : {
-  name = "kernel-testing";
-  meta = with pkgs.lib.maintainers; {
-    maintainers = [ nequissimus ];
-  };
-
-  machine = { pkgs, ... }:
-    {
-      boot.kernelPackages = pkgs.linuxPackages_testing;
-    };
-
-  testScript =
-    ''
-      assert "Linux" in machine.succeed("uname -s")
-      assert "${pkgs.linuxPackages_testing.kernel.modDirVersion}" in machine.succeed("uname -a")
-    '';
-})
diff --git a/nixos/tests/keycloak.nix b/nixos/tests/keycloak.nix
index 45d8677af56..136e83b3e02 100644
--- a/nixos/tests/keycloak.nix
+++ b/nixos/tests/keycloak.nix
@@ -20,6 +20,7 @@ let
           services.keycloak = {
             enable = true;
             inherit frontendUrl databaseType initialAdminPassword;
+            databaseUsername = "bogus";
             databasePasswordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH";
           };
           environment.systemPackages = with pkgs; [
diff --git a/nixos/tests/kubernetes/dns.nix b/nixos/tests/kubernetes/dns.nix
index 890499a0fb8..b6cd811c5ae 100644
--- a/nixos/tests/kubernetes/dns.nix
+++ b/nixos/tests/kubernetes/dns.nix
@@ -34,7 +34,7 @@ let
     name = "redis";
     tag = "latest";
     contents = [ pkgs.redis pkgs.bind.host ];
-    config.Entrypoint = "/bin/redis-server";
+    config.Entrypoint = ["/bin/redis-server"];
   };
 
   probePod = pkgs.writeText "probe-pod.json" (builtins.toJSON {
@@ -55,12 +55,11 @@ let
     name = "probe";
     tag = "latest";
     contents = [ pkgs.bind.host pkgs.busybox ];
-    config.Entrypoint = "/bin/tail";
+    config.Entrypoint = ["/bin/tail"];
   };
 
-  extraConfiguration = { config, pkgs, ... }: {
+  extraConfiguration = { config, pkgs, lib, ... }: {
     environment.systemPackages = [ pkgs.bind.host ];
-    # virtualisation.docker.extraOptions = "--dns=${config.services.kubernetes.addons.dns.clusterIp}";
     services.dnsmasq.enable = true;
     services.dnsmasq.servers = [
       "/cluster.local/${config.services.kubernetes.addons.dns.clusterIp}#53"
@@ -77,7 +76,7 @@ let
       # prepare machine1 for test
       machine1.wait_until_succeeds("kubectl get node machine1.${domain} | grep -w Ready")
       machine1.wait_until_succeeds(
-          "docker load < ${redisImage}"
+          "${pkgs.gzip}/bin/zcat ${redisImage} | ${pkgs.containerd}/bin/ctr -n k8s.io image import -"
       )
       machine1.wait_until_succeeds(
           "kubectl create -f ${redisPod}"
@@ -86,7 +85,7 @@ let
           "kubectl create -f ${redisService}"
       )
       machine1.wait_until_succeeds(
-          "docker load < ${probeImage}"
+          "${pkgs.gzip}/bin/zcat ${probeImage} | ${pkgs.containerd}/bin/ctr -n k8s.io image import -"
       )
       machine1.wait_until_succeeds(
           "kubectl create -f ${probePod}"
@@ -118,7 +117,7 @@ let
       # prepare machines for test
       machine1.wait_until_succeeds("kubectl get node machine2.${domain} | grep -w Ready")
       machine2.wait_until_succeeds(
-          "docker load < ${redisImage}"
+          "${pkgs.gzip}/bin/zcat ${redisImage} | ${pkgs.containerd}/bin/ctr -n k8s.io image import -"
       )
       machine1.wait_until_succeeds(
           "kubectl create -f ${redisPod}"
@@ -127,7 +126,7 @@ let
           "kubectl create -f ${redisService}"
       )
       machine2.wait_until_succeeds(
-          "docker load < ${probeImage}"
+          "${pkgs.gzip}/bin/zcat ${probeImage} | ${pkgs.containerd}/bin/ctr -n k8s.io image import -"
       )
       machine1.wait_until_succeeds(
           "kubectl create -f ${probePod}"
diff --git a/nixos/tests/kubernetes/rbac.nix b/nixos/tests/kubernetes/rbac.nix
index c922da515d9..3fc8ed0fbe3 100644
--- a/nixos/tests/kubernetes/rbac.nix
+++ b/nixos/tests/kubernetes/rbac.nix
@@ -85,7 +85,7 @@ let
     name = "kubectl";
     tag = "latest";
     contents = [ kubectl pkgs.busybox kubectlPod2 ];
-    config.Entrypoint = "/bin/sh";
+    config.Entrypoint = ["/bin/sh"];
   };
 
   base = {
@@ -97,7 +97,7 @@ let
       machine1.wait_until_succeeds("kubectl get node machine1.my.zyx | grep -w Ready")
 
       machine1.wait_until_succeeds(
-          "docker load < ${kubectlImage}"
+          "${pkgs.gzip}/bin/zcat ${kubectlImage} | ${pkgs.containerd}/bin/ctr -n k8s.io image import -"
       )
 
       machine1.wait_until_succeeds(
@@ -134,7 +134,7 @@ let
       machine1.wait_until_succeeds("kubectl get node machine2.my.zyx | grep -w Ready")
 
       machine2.wait_until_succeeds(
-          "docker load < ${kubectlImage}"
+          "${pkgs.gzip}/bin/zcat ${kubectlImage} | ${pkgs.containerd}/bin/ctr -n k8s.io image import -"
       )
 
       machine1.wait_until_succeeds(
diff --git a/nixos/tests/matrix-appservice-irc.nix b/nixos/tests/matrix-appservice-irc.nix
new file mode 100644
index 00000000000..79b07ef83c5
--- /dev/null
+++ b/nixos/tests/matrix-appservice-irc.nix
@@ -0,0 +1,162 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
+    homeserverUrl = "http://homeserver:8448";
+  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;
+            database_type = "sqlite3";
+            app_service_config_files = [ "/registration.yml" ];
+
+            enable_registration = true;
+
+            listeners = [
+              # The default but tls=false
+              {
+                "bind_address" = "";
+                "port" = 8448;
+                "resources" = [
+                  { "compress" = true; "names" = [ "client" "webclient" ]; }
+                  { "compress" = false; "names" = [ "federation" ]; }
+                ];
+                "tls" = false;
+                "type" = "http";
+                "x_forwarded" = false;
+              }
+            ];
+          };
+
+          networking.firewall.allowedTCPPorts = [ 8448 ];
+        };
+      };
+
+      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-client ]; } ''
+            import socket
+            from matrix_client.client import MatrixClient
+            from time import sleep
+
+            matrix = MatrixClient("${homeserverUrl}")
+            matrix.register_with_password(username="alice", password="foobar")
+
+            irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            irc.connect(("ircd", 6667))
+            irc.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+            irc.send(b"USER bob bob bob :bob\n")
+            irc.send(b"NICK bob\n")
+
+            m_room = matrix.join_room("#irc_#test:homeserver")
+            irc.send(b"JOIN #test\n")
+
+            # plenty of time for the joins to happen
+            sleep(10)
+
+            m_room.send_text("hi from matrix")
+            irc.send(b"PRIVMSG #test :hi from irc \r\n")
+
+            print("Waiting for irc message...")
+            while True:
+                buf = irc.recv(10000)
+                if b"hi from matrix" in buf:
+                    break
+
+            print("Waiting for matrix message...")
+
+
+            def callback(room, e):
+                if "hi from irc" in e['content']['body']:
+                    exit(0)
+
+
+            m_room.add_listener(callback, "m.room.message")
+            matrix.listen_forever()
+          ''
+          )
+        ];
+      };
+    };
+
+    testScript = ''
+      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(8448)
+
+      with subtest("ensure messages can be exchanged"):
+          client.succeed("do_test")
+    '';
+
+  })
diff --git a/nixos/tests/miniflux.nix b/nixos/tests/miniflux.nix
index 9f8b52c3c85..797a2787d1a 100644
--- a/nixos/tests/miniflux.nix
+++ b/nixos/tests/miniflux.nix
@@ -20,6 +20,13 @@ with lib;
         services.miniflux.enable = true;
       };
 
+    withoutSudo =
+      { ... }:
+      {
+        services.miniflux.enable = true;
+        security.sudo.enable = false;
+      };
+
     customized =
       { ... }:
       {
@@ -46,6 +53,13 @@ with lib;
         "curl 'http://localhost:${toString defaultPort}/v1/me' -u '${defaultUsername}:${defaultPassword}' -H Content-Type:application/json | grep -q '\"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 -q OK")
+    withoutSudo.succeed(
+        "curl 'http://localhost:${toString defaultPort}/v1/me' -u '${defaultUsername}:${defaultPassword}' -H Content-Type:application/json | grep -q '\"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 -q OK")
diff --git a/nixos/tests/mosquitto.nix b/nixos/tests/mosquitto.nix
index 308c1396013..e29bd559ed9 100644
--- a/nixos/tests/mosquitto.nix
+++ b/nixos/tests/mosquitto.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ pkgs, ... }:
+import ./make-test-python.nix ({ pkgs, lib, ... }:
 
 let
   port = 1888;
@@ -30,6 +30,9 @@ in {
           ];
         };
       };
+
+      # disable private /tmp for this test
+      systemd.services.mosquitto.serviceConfig.PrivateTmp = lib.mkForce false;
     };
 
     client1 = client;
diff --git a/nixos/tests/mxisd.nix b/nixos/tests/mxisd.nix
index 22755ea353b..354612a8a53 100644
--- a/nixos/tests/mxisd.nix
+++ b/nixos/tests/mxisd.nix
@@ -6,25 +6,16 @@ import ./make-test-python.nix ({ pkgs, ... } : {
   };
 
   nodes = {
-    server_mxisd = args : {
+    server = args : {
       services.mxisd.enable = true;
       services.mxisd.matrix.domain = "example.org";
     };
-
-    server_ma1sd = args : {
-      services.mxisd.enable = true;
-      services.mxisd.matrix.domain = "example.org";
-      services.mxisd.package = pkgs.ma1sd;
-    };
   };
 
   testScript = ''
     start_all()
-    server_mxisd.wait_for_unit("mxisd.service")
-    server_mxisd.wait_for_open_port(8090)
-    server_mxisd.succeed("curl -Ssf 'http://127.0.0.1:8090/_matrix/identity/api/v1'")
-    server_ma1sd.wait_for_unit("mxisd.service")
-    server_ma1sd.wait_for_open_port(8090)
-    server_ma1sd.succeed("curl -Ssf 'http://127.0.0.1:8090/_matrix/identity/api/v1'")
+    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/mariadb-galera-mariabackup.nix b/nixos/tests/mysql/mariadb-galera-mariabackup.nix
index a4b893a9f33..1c73bc854a5 100644
--- a/nixos/tests/mysql/mariadb-galera-mariabackup.nix
+++ b/nixos/tests/mysql/mariadb-galera-mariabackup.nix
@@ -2,7 +2,7 @@ import ./../make-test-python.nix ({ pkgs, ...} :
 
 let
   mysqlenv-common      = pkgs.buildEnv { name = "mysql-path-env-common";      pathsToLink = [ "/bin" ]; paths = with pkgs; [ bash gawk gnutar inetutils which ]; };
-  mysqlenv-mariabackup = pkgs.buildEnv { name = "mysql-path-env-mariabackup"; pathsToLink = [ "/bin" ]; paths = with pkgs; [ gzip iproute netcat procps pv socat ]; };
+  mysqlenv-mariabackup = pkgs.buildEnv { name = "mysql-path-env-mariabackup"; pathsToLink = [ "/bin" ]; paths = with pkgs; [ gzip iproute2 netcat procps pv socat ]; };
 
 in {
   name = "mariadb-galera-mariabackup";
@@ -31,7 +31,7 @@ in {
         firewall.allowedTCPPorts = [ 3306 4444 4567 4568 ];
         firewall.allowedUDPPorts = [ 4567 ];
       };
-      users.users.testuser = { };
+      users.users.testuser = { isSystemUser = true; };
       systemd.services.mysql = with pkgs; {
         path = [ mysqlenv-common mysqlenv-mariabackup ];
       };
@@ -89,7 +89,7 @@ in {
         firewall.allowedTCPPorts = [ 3306 4444 4567 4568 ];
         firewall.allowedUDPPorts = [ 4567 ];
       };
-      users.users.testuser = { };
+      users.users.testuser = { isSystemUser = true; };
       systemd.services.mysql = with pkgs; {
         path = [ mysqlenv-common mysqlenv-mariabackup ];
       };
@@ -136,7 +136,7 @@ in {
         firewall.allowedTCPPorts = [ 3306 4444 4567 4568 ];
         firewall.allowedUDPPorts = [ 4567 ];
       };
-      users.users.testuser = { };
+      users.users.testuser = { isSystemUser = true; };
       systemd.services.mysql = with pkgs; {
         path = [ mysqlenv-common mysqlenv-mariabackup ];
       };
diff --git a/nixos/tests/mysql/mariadb-galera-rsync.nix b/nixos/tests/mysql/mariadb-galera-rsync.nix
index 6fb3cfef8d7..709a8b5085c 100644
--- a/nixos/tests/mysql/mariadb-galera-rsync.nix
+++ b/nixos/tests/mysql/mariadb-galera-rsync.nix
@@ -31,7 +31,7 @@ in {
         firewall.allowedTCPPorts = [ 3306 4444 4567 4568 ];
         firewall.allowedUDPPorts = [ 4567 ];
       };
-      users.users.testuser = { };
+      users.users.testuser = { isSystemUser = true; };
       systemd.services.mysql = with pkgs; {
         path = [ mysqlenv-common mysqlenv-rsync ];
       };
@@ -84,7 +84,7 @@ in {
         firewall.allowedTCPPorts = [ 3306 4444 4567 4568 ];
         firewall.allowedUDPPorts = [ 4567 ];
       };
-      users.users.testuser = { };
+      users.users.testuser = { isSystemUser = true; };
       systemd.services.mysql = with pkgs; {
         path = [ mysqlenv-common mysqlenv-rsync ];
       };
@@ -130,7 +130,7 @@ in {
         firewall.allowedTCPPorts = [ 3306 4444 4567 4568 ];
         firewall.allowedUDPPorts = [ 4567 ];
       };
-      users.users.testuser = { };
+      users.users.testuser = { isSystemUser = true; };
       systemd.services.mysql = with pkgs; {
         path = [ mysqlenv-common mysqlenv-rsync ];
       };
diff --git a/nixos/tests/mysql/mysql.nix b/nixos/tests/mysql/mysql.nix
index 50ad5c68aef..c21136416d4 100644
--- a/nixos/tests/mysql/mysql.nix
+++ b/nixos/tests/mysql/mysql.nix
@@ -9,8 +9,8 @@ import ./../make-test-python.nix ({ pkgs, ...} : {
       { pkgs, ... }:
 
       {
-        users.users.testuser = { };
-        users.users.testuser2 = { };
+        users.users.testuser = { isSystemUser = true; };
+        users.users.testuser2 = { isSystemUser = true; };
         services.mysql.enable = true;
         services.mysql.initialDatabases = [
           { name = "testdb3"; schema = ./testdb.sql; }
@@ -44,8 +44,8 @@ import ./../make-test-python.nix ({ pkgs, ...} : {
         # Kernel panic - not syncing: Out of memory: compulsory panic_on_oom is enabled
         virtualisation.memorySize = 1024;
 
-        users.users.testuser = { };
-        users.users.testuser2 = { };
+        users.users.testuser = { isSystemUser = true; };
+        users.users.testuser2 = { isSystemUser = true; };
         services.mysql.enable = true;
         services.mysql.initialDatabases = [
           { name = "testdb3"; schema = ./testdb.sql; }
@@ -75,8 +75,8 @@ import ./../make-test-python.nix ({ pkgs, ...} : {
       { pkgs, ... }:
 
       {
-        users.users.testuser = { };
-        users.users.testuser2 = { };
+        users.users.testuser = { isSystemUser = true; };
+        users.users.testuser2 = { isSystemUser = true; };
         services.mysql.enable = true;
         services.mysql.initialScript = pkgs.writeText "mariadb-init.sql" ''
           ALTER USER root@localhost IDENTIFIED WITH unix_socket;
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/nextcloud/basic.nix b/nixos/tests/nextcloud/basic.nix
index 0b8e1937128..76f7f68dc96 100644
--- a/nixos/tests/nextcloud/basic.nix
+++ b/nixos/tests/nextcloud/basic.nix
@@ -7,7 +7,7 @@ in {
     maintainers = [ globin eqyiel ];
   };
 
-  nodes = {
+  nodes = rec {
     # The only thing the client needs to do is download a file.
     client = { ... }: {
       services.davfs2.enable = true;
@@ -47,9 +47,14 @@ in {
 
       environment.systemPackages = [ cfg.services.nextcloud.occ ];
     };
+
+    nextcloudWithoutMagick = args@{ config, pkgs, lib, ... }:
+      lib.mkMerge
+      [ (nextcloud args)
+        { services.nextcloud.enableImagemagick = false; } ];
   };
 
-  testScript = let
+  testScript = { nodes, ... }: let
     withRcloneEnv = pkgs.writeScript "with-rclone-env" ''
       #!${pkgs.runtimeShell}
       export RCLONE_CONFIG_NEXTCLOUD_TYPE=webdav
@@ -68,8 +73,19 @@ in {
       #!${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 ''
-    start_all()
+    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")
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/os-prober.nix b/nixos/tests/os-prober.nix
index f778d30bdc0..3cc38ebe347 100644
--- a/nixos/tests/os-prober.nix
+++ b/nixos/tests/os-prober.nix
@@ -69,6 +69,9 @@ in {
       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;
@@ -95,11 +98,6 @@ in {
   });
 
   testScript = ''
-    # hack to add the secondary disk
-    os.environ[
-        "QEMU_OPTS"
-    ] = "-drive index=2,file=${debianImage}/disk-image.qcow2,read-only,if=virtio"
-
     machine.start()
     machine.succeed("udevadm settle")
     machine.wait_for_unit("multi-user.target")
diff --git a/nixos/tests/packagekit.nix b/nixos/tests/packagekit.nix
index 28d1374bf92..020a4e65e6d 100644
--- a/nixos/tests/packagekit.nix
+++ b/nixos/tests/packagekit.nix
@@ -8,7 +8,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     environment.systemPackages = with pkgs; [ dbus ];
     services.packagekit = {
       enable = true;
-      backend = "test_nop";
     };
   };
 
diff --git a/nixos/tests/php/default.nix b/nixos/tests/php/default.nix
index ee7a3b56a3e..cf78c9db53b 100644
--- a/nixos/tests/php/default.nix
+++ b/nixos/tests/php/default.nix
@@ -1,8 +1,15 @@
-{ system ? builtins.currentSystem,
-  config ? {},
-  pkgs ? import ../../.. { inherit system config; }
-}: {
-  fpm = import ./fpm.nix { inherit system pkgs; };
-  httpd = import ./httpd.nix { inherit system pkgs; };
-  pcre = import ./pcre.nix { inherit system pkgs; };
+{ 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
index 9ad515ebdde..b11f85d39cb 100644
--- a/nixos/tests/php/fpm.nix
+++ b/nixos/tests/php/fpm.nix
@@ -1,8 +1,10 @@
-import ../make-test-python.nix ({pkgs, lib, ...}: {
-  name = "php-fpm-nginx-test";
+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;
 
@@ -10,7 +12,7 @@ import ../make-test-python.nix ({pkgs, lib, ...}: {
         testdir = pkgs.writeTextDir "web/index.php" "<?php phpinfo();";
       in {
         root = "${testdir}/web";
-        locations."~ \.php$".extraConfig = ''
+        locations."~ \\.php$".extraConfig = ''
           fastcgi_pass unix:${config.services.phpfpm.pools.foobar.socket};
           fastcgi_index index.php;
           include ${pkgs.nginx}/conf/fastcgi_params;
@@ -25,6 +27,7 @@ import ../make-test-python.nix ({pkgs, lib, ...}: {
 
     services.phpfpm.pools."foobar" = {
       user = "nginx";
+      phpPackage = php;
       settings = {
         "listen.group" = "nginx";
         "listen.mode" = "0600";
@@ -44,10 +47,11 @@ import ../make-test-python.nix ({pkgs, lib, ...}: {
 
     # Check so we get an evaluated PHP back
     response = machine.succeed("curl -fvvv -s http://127.0.0.1:80/")
-    assert "PHP Version ${pkgs.php.version}" in response, "PHP version not detected"
+    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"]:
+    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
index 27ea7a24e3a..a5ca9b3c5d1 100644
--- a/nixos/tests/php/httpd.nix
+++ b/nixos/tests/php/httpd.nix
@@ -1,5 +1,5 @@
-import ../make-test-python.nix ({pkgs, lib, ...}: {
-  name = "php-httpd-test";
+import ../make-test-python.nix ({pkgs, lib, php, ...}: {
+  name = "php-${php.version}-httpd-test";
   meta.maintainers = lib.teams.php.members;
 
   machine = { config, lib, pkgs, ... }: {
@@ -14,6 +14,7 @@ import ../make-test-python.nix ({pkgs, lib, ...}: {
           index = "index.php index.html";
         };
       };
+      phpPackage = php;
       enablePHP = true;
     };
   };
@@ -22,7 +23,7 @@ import ../make-test-python.nix ({pkgs, lib, ...}: {
 
     # Check so we get an evaluated PHP back
     response = machine.succeed("curl -fvvv -s http://127.0.0.1:80/")
-    assert "PHP Version ${pkgs.php.version}" in response, "PHP version not detected"
+    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"]:
diff --git a/nixos/tests/php/pcre.nix b/nixos/tests/php/pcre.nix
index 3ea19304bff..97572f63af3 100644
--- a/nixos/tests/php/pcre.nix
+++ b/nixos/tests/php/pcre.nix
@@ -1,7 +1,7 @@
 let
   testString = "can-use-subgroups";
-in import ../make-test-python.nix ({lib, ...}: {
-  name = "php-httpd-pcre-jit-test";
+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, ... }: {
@@ -9,6 +9,7 @@ in import ../make-test-python.nix ({lib, ...}: {
     services.httpd = {
       enable = true;
       adminAddr = "please@dont.contact";
+      phpPackage = php;
       enablePHP = true;
       phpOptions = "pcre.jit = true";
       extraConfig = let
diff --git a/nixos/tests/pinnwand.nix b/nixos/tests/pinnwand.nix
index 0c583e1104d..0391c413311 100644
--- a/nixos/tests/pinnwand.nix
+++ b/nixos/tests/pinnwand.nix
@@ -61,7 +61,7 @@ in
     client.wait_until_succeeds("ping -c1 server")
 
     # make sure pinnwand is listening
-    server.wait_until_succeeds("ss -lnp | grep ${toString port}")
+    server.wait_for_open_port(${toString port})
 
     # send the contents of /etc/machine-id
     response = client.succeed("steck paste /etc/machine-id")
@@ -75,6 +75,12 @@ in
         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")
@@ -82,5 +88,7 @@ in
     # 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/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/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.nix b/nixos/tests/podman.nix
index 4985ff60365..6078a936ede 100644
--- a/nixos/tests/podman.nix
+++ b/nixos/tests/podman.nix
@@ -96,6 +96,15 @@ import ./make-test-python.nix (
           podman.succeed(su_cmd("podman ps | grep sleeping"))
           podman.succeed(su_cmd("podman stop sleeping"))
           podman.succeed(su_cmd("podman rm sleeping"))
+
+      with subtest("Run container 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"
     '';
   }
 )
diff --git a/nixos/tests/pomerium.nix b/nixos/tests/pomerium.nix
new file mode 100644
index 00000000000..531b6212711
--- /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 -q 'hello world'"
+        )
+
+    with subtest("login required"):
+        pomerium.succeed(
+            "curl -I --resolve login.required:80:127.0.0.1 http://login.required | grep -q pom-auth"
+        )
+        pomerium.succeed(
+            "curl -L --resolve login.required:80:127.0.0.1 http://login.required | grep -q 'hello I am login page'"
+        )
+  '';
+})
diff --git a/nixos/tests/privacyidea.nix b/nixos/tests/privacyidea.nix
index b71ff0a1669..4a94f072794 100644
--- a/nixos/tests/privacyidea.nix
+++ b/nixos/tests/privacyidea.nix
@@ -12,10 +12,16 @@ import ./make-test-python.nix ({ pkgs, ...} : rec {
 
     services.privacyidea = {
       enable = true;
-      secretKey = "testing";
-      pepper = "testing";
+      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;
@@ -29,6 +35,8 @@ import ./make-test-python.nix ({ pkgs, ...} : rec {
     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
index 89d17c9de8c..2b17d0ff78f 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -1,65 +1,65 @@
 { system ? builtins.currentSystem
-, config ? {}
+, 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()
- *    '';
- */
+    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 = {
+    apcupsd = {
       exporterConfig = {
         enable = true;
       };
@@ -75,6 +75,21 @@ let
       '';
     };
 
+    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 -q 'artifactory_up'"
+        )
+      '';
+    };
+
     bind = {
       exporterConfig = {
         enable = true;
@@ -103,6 +118,8 @@ let
       metricProvider = {
         services.bird2.enable = true;
         services.bird2.config = ''
+          router id 127.0.0.1;
+
           protocol kernel MyObviousTestString {
             ipv4 {
               import all;
@@ -117,7 +134,27 @@ let
       exporterTest = ''
         wait_for_unit("prometheus-bird-exporter.service")
         wait_for_open_port(9324)
-        succeed("curl -sSf http://localhost:9324/metrics | grep -q 'MyObviousTestString'")
+        wait_until_succeeds(
+            "curl -sSf http://localhost:9324/metrics | grep -q '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 -q '^bitcoin_blocks '")
       '';
     };
 
@@ -155,20 +192,21 @@ let
           "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 -q 'collectd_testplugin_gauge{instance=\"testhost\"} 23'"
-        )
-      '';
+      ''; 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 -q 'collectd_testplugin_gauge{instance=\"testhost\"} 23'"
+          )
+        '';
     };
 
     dnsmasq = {
@@ -186,6 +224,22 @@ let
       '';
     };
 
+    # 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 -q 'domain_probe_success 0'"
+        )
+      '';
+    };
+
     dovecot = {
       exporterConfig = {
         enable = true;
@@ -205,7 +259,8 @@ let
       '';
     };
 
-    fritzbox = { # TODO add proper test case
+    fritzbox = {
+      # TODO add proper test case
       exporterConfig = {
         enable = true;
       };
@@ -218,6 +273,29 @@ let
       '';
     };
 
+    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 -q "key=participants"'
+        )
+        succeed("curl -sSf 'localhost:9700/metrics' | grep -q 'jitsi_participants 0'")
+      '';
+    };
+
     json = {
       exporterConfig = {
         enable = true;
@@ -248,6 +326,59 @@ let
       '';
     };
 
+    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 -q '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
@@ -283,19 +414,19 @@ let
         '';
         systemd.services.lnd = {
           serviceConfig.ExecStart = ''
-          ${pkgs.lnd}/bin/lnd \
-            --datadir=/var/lib/lnd \
-            --tlscertpath=/var/lib/lnd/tls.cert \
-            --tlskeypath=/var/lib/lnd/tls.key \
-            --logdir=/var/log/lnd \
-            --bitcoin.active \
-            --bitcoin.mainnet \
-            --bitcoin.node=bitcoind \
-            --bitcoind.rpcuser=bitcoinrpc \
-            --bitcoind.rpcpass=hunter2 \
-            --bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332 \
-            --bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333 \
-            --readonlymacaroonpath=/var/lib/lnd/readonly.macaroon
+            ${pkgs.lnd}/bin/lnd \
+              --datadir=/var/lib/lnd \
+              --tlscertpath=/var/lib/lnd/tls.cert \
+              --tlskeypath=/var/lib/lnd/tls.key \
+              --logdir=/var/log/lnd \
+              --bitcoin.active \
+              --bitcoin.mainnet \
+              --bitcoin.node=bitcoind \
+              --bitcoind.rpcuser=bitcoinrpc \
+              --bitcoind.rpcpass=hunter2 \
+              --bitcoind.zmqpubrawblock=tcp://127.0.0.1:28332 \
+              --bitcoind.zmqpubrawtx=tcp://127.0.0.1:28333 \
+              --readonlymacaroonpath=/var/lib/lnd/readonly.macaroon
           '';
           serviceConfig.StateDirectory = "lnd";
           wantedBy = [ "multi-user.target" ];
@@ -317,14 +448,14 @@ let
         configuration = {
           monitoringInterval = "2s";
           mailCheckTimeout = "10s";
-          servers = [ {
+          servers = [{
             name = "testserver";
             server = "localhost";
             port = 25;
             from = "mail-exporter@localhost";
             to = "mail-exporter@localhost";
             detectionDir = "/var/spool/mail/mail-exporter/new";
-          } ];
+          }];
         };
       };
       metricProvider = {
@@ -426,15 +557,17 @@ let
         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
-          '';
-        };
+        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" = {
@@ -491,7 +624,7 @@ let
                 syslog = {
                   listen_address = "udp://127.0.0.1:10000";
                   format = "rfc3164";
-                  tags = ["nginx"];
+                  tags = [ "nginx" ];
                 };
               };
             }
@@ -547,14 +680,74 @@ let
       '';
     };
 
+    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 -q 'openldap_scrape{result=\"ok\"} 1'"
+        )
+      '';
+    };
+
     openvpn = {
       exporterConfig = {
         enable = true;
         group = "openvpn";
-        statusPaths = ["/run/openvpn-test"];
+        statusPaths = [ "/run/openvpn-test" ];
       };
       metricProvider = {
-        users.groups.openvpn = {};
+        users.groups.openvpn = { };
         services.openvpn.servers.test = {
           config = ''
             dev tun
@@ -674,19 +867,21 @@ let
       };
       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"
-          '';
-        }) ];
+        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")
@@ -702,7 +897,7 @@ let
     smokeping = {
       exporterConfig = {
         enable = true;
-        hosts = ["127.0.0.1"];
+        hosts = [ "127.0.0.1" ];
       };
       exporterTest = ''
         wait_for_unit("prometheus-smokeping-exporter.service")
@@ -802,6 +997,22 @@ let
       '';
     };
 
+    systemd = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = { };
+      exporterTest = ''
+        wait_for_unit("prometheus-systemd-exporter.service")
+        wait_for_open_port(9558)
+        succeed(
+            "curl -sSf localhost:9558/metrics | grep -q '{}'".format(
+                'systemd_unit_state{name="basic.target",state="active",type="target"} 1'
+            )
+        )
+      '';
+    };
+
     tor = {
       exporterConfig = {
         enable = true;
@@ -824,7 +1035,7 @@ let
     unifi-poller = {
       nodeName = "unifi_poller";
       exporterConfig.enable = true;
-      exporterConfig.controllers = [ { } ];
+      exporterConfig.controllers = [{ }];
       exporterTest = ''
         wait_for_unit("prometheus-unifi-poller-exporter.service")
         wait_for_open_port(9130)
@@ -834,6 +1045,29 @@ let
       '';
     };
 
+    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 -q 'unbound_up 1'")
+      '';
+    };
+
     varnish = {
       exporterConfig = {
         enable = true;
@@ -863,54 +1097,60 @@ let
       '';
     };
 
-    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;
+    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;
+            inherit (snakeoil.peer0) privateKey;
 
-          peers = singleton {
-            allowedIPs = [ "10.23.42.2/32" "fc00::2/128" ];
+            peers = singleton {
+              allowedIPs = [ "10.23.42.2/32" "fc00::2/128" ];
 
-            inherit (snakeoil.peer1) publicKey;
+              inherit (snakeoil.peer1) publicKey;
+            };
           };
+          systemd.services.prometheus-wireguard-exporter.after = [ "wireguard-wg0.service" ];
         };
-        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}'"
+          )
+        '';
       };
-      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
+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/quagga.nix b/nixos/tests/quagga.nix
deleted file mode 100644
index 9aed49bf452..00000000000
--- a/nixos/tests/quagga.nix
+++ /dev/null
@@ -1,96 +0,0 @@
-# This test runs Quagga 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;
-
-    ospfConf = ''
-      interface eth2
-        ip ospf hello-interval 1
-        ip ospf dead-interval 5
-      !
-      router ospf
-        network 192.168.0.0/16 area 0
-    '';
-
-  in
-    {
-      name = "quagga";
-
-      meta = with pkgs.lib.maintainers; {
-        maintainers = [ tavyc ];
-      };
-
-      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 ospf -j ACCEPT";
-            services.quagga.ospf = {
-              enable = true;
-              config = ospfConf;
-            };
-          };
-
-        router2 =
-          { ... }:
-          {
-            virtualisation.vlans = [ 3 2 ];
-            boot.kernel.sysctl."net.ipv4.ip_forward" = "1";
-            networking.firewall.extraCommands = "iptables -A nixos-fw -i eth2 -p ospf -j ACCEPT";
-            services.quagga.ospf = {
-              enable = true;
-              config = ospfConf;
-            };
-          };
-
-        server =
-          { nodes, ... }:
-          {
-            virtualisation.vlans = [ 3 ];
-            networking.defaultGateway = ifAddr nodes.router2 "eth1";
-            networking.firewall.allowedTCPPorts = [ 80 ];
-            services.httpd.enable = true;
-            services.httpd.adminAddr = "foo@example.com";
-          };
-      };
-
-      testScript =
-        { ... }:
-        ''
-          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 OSPF to form adjacencies"):
-              for gw in router1, router2:
-                  gw.wait_for_unit("ospfd")
-                  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")
-
-          with subtest("Test whether HTTP works"):
-              server.wait_for_unit("httpd")
-              client.succeed("curl --fail http://server/ >&2")
-        '';
-    })
diff --git a/nixos/tests/redis.nix b/nixos/tests/redis.nix
index ca171561435..28b6058c2c0 100644
--- a/nixos/tests/redis.nix
+++ b/nixos/tests/redis.nix
@@ -17,16 +17,15 @@ in
         services.redis.unixSocket = redisSocket;
 
         # Allow access to the unix socket for the "redis" group.
-        services.redis.settings.unixsocketperm = "770";
+        services.redis.unixSocketPerm = 770;
 
         users.users."member" = {
           createHome = false;
           description = "A member of the redis group";
+          isNormalUser = true;
           extraGroups = [
             "redis"
           ];
-          group = "users";
-          shell = "/bin/sh";
         };
       };
   };
diff --git a/nixos/tests/restic.nix b/nixos/tests/restic.nix
index 0cc8bd39afb..16979eab821 100644
--- a/nixos/tests/restic.nix
+++ b/nixos/tests/restic.nix
@@ -45,6 +45,10 @@ import ./make-test-python.nix (
                     '';
                     inherit passwordFile initialize paths pruneOpts;
                   };
+                  remoteprune = {
+                    inherit repository passwordFile;
+                    pruneOpts = [ "--keep-last 1" ];
+                  };
                 };
 
                 environment.sessionVariables.RCLONE_CONFIG_LOCAL_TYPE = "local";
@@ -84,6 +88,8 @@ import ./make-test-python.nix (
               "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/rspamd.nix b/nixos/tests/rspamd.nix
index 7f41e1a7956..3fd55444fd8 100644
--- a/nixos/tests/rspamd.nix
+++ b/nixos/tests/rspamd.nix
@@ -25,6 +25,7 @@ let
     machine = {
       services.rspamd.enable = true;
       networking.enableIPv6 = enableIPv6;
+      virtualisation.memorySize = 1024;
     };
     testScript = ''
       start_all()
@@ -68,6 +69,7 @@ in
           group = "rspamd";
         }];
       };
+      virtualisation.memorySize = 1024;
     };
 
     testScript = ''
@@ -116,6 +118,7 @@ in
           '';
         };
       };
+      virtualisation.memorySize = 1024;
     };
 
     testScript = ''
@@ -221,6 +224,7 @@ in
           rspamd_logger.infox(rspamd_config, 'Work dammit!!!')
         '';
       };
+      virtualisation.memorySize = 1024;
     };
     testScript = ''
       ${initMachine}
@@ -274,7 +278,10 @@ in
 
         I find cows to be evil don't you?
       '';
-      users.users.tester.password = "test";
+      users.users.tester = {
+        isNormalUser = true;
+        password = "test";
+      };
       services.postfix = {
         enable = true;
         destination = ["example.com"];
@@ -284,6 +291,7 @@ in
         postfix.enable = true;
         workers.rspamd_proxy.type = "rspamd_proxy";
       };
+      virtualisation.memorySize = 1024;
     };
     testScript = ''
       ${initMachine}
diff --git a/nixos/tests/searx.nix b/nixos/tests/searx.nix
index 7c28eea30d2..2f808cb6526 100644
--- a/nixos/tests/searx.nix
+++ b/nixos/tests/searx.nix
@@ -108,7 +108,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
               "${pkgs.curl}/bin/curl --fail http://localhost/searx >&2"
           )
           fancy.succeed(
-              "${pkgs.curl}/bin/curl --fail http://localhost/searx/static/js/bootstrap.min.js >&2"
+              "${pkgs.curl}/bin/curl --fail http://localhost/searx/static/themes/oscar/js/bootstrap.min.js >&2"
           )
     '';
 })
diff --git a/nixos/tests/shadow.nix b/nixos/tests/shadow.nix
index e5755e8e087..c51961e1fc6 100644
--- a/nixos/tests/shadow.nix
+++ b/nixos/tests/shadow.nix
@@ -13,14 +13,17 @@ in import ./make-test-python.nix ({ pkgs, ... }: {
     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;
       };
diff --git a/nixos/tests/snapcast.nix b/nixos/tests/snapcast.nix
index 2fef6362514..ef35d586c9c 100644
--- a/nixos/tests/snapcast.nix
+++ b/nixos/tests/snapcast.nix
@@ -34,6 +34,10 @@ in {
             type = "tcp";
             location = "127.0.0.1:${toString tcpStreamPort}";
           };
+          meta = {
+            type = "meta";
+            location = "/mpd/bluetooth/tcp";
+          };
         };
       };
     };
diff --git a/nixos/tests/spacecookie.nix b/nixos/tests/spacecookie.nix
index 5b5022a7427..a640657d8a6 100644
--- a/nixos/tests/spacecookie.nix
+++ b/nixos/tests/spacecookie.nix
@@ -1,47 +1,52 @@
 let
-  gopherRoot  = "/tmp/gopher";
-  gopherHost  = "gopherd";
-  fileContent = "Hello Gopher!";
-  fileName    = "file.txt";
+  gopherRoot   = "/tmp/gopher";
+  gopherHost   = "gopherd";
+  gopherClient = "client";
+  fileContent  = "Hello Gopher!\n";
+  fileName     = "file.txt";
 in
   import ./make-test-python.nix ({...}: {
     name = "spacecookie";
     nodes = {
       ${gopherHost} = {
-        networking.firewall.allowedTCPPorts = [ 70 ];
         systemd.services.spacecookie = {
           preStart = ''
             mkdir -p ${gopherRoot}/directory
-            echo "${fileContent}" > ${gopherRoot}/${fileName}
+            printf "%s" "${fileContent}" > ${gopherRoot}/${fileName}
           '';
         };
 
         services.spacecookie = {
           enable = true;
-          root = gopherRoot;
-          hostname = gopherHost;
+          openFirewall = true;
+          settings = {
+            root = gopherRoot;
+            hostname = gopherHost;
+          };
         };
       };
 
-      client = {};
+      ${gopherClient} = {};
     };
 
     testScript = ''
       start_all()
-      ${gopherHost}.wait_for_open_port(70)
+
+      # with daemon type notify, the unit being started
+      # should also mean the port is open
       ${gopherHost}.wait_for_unit("spacecookie.service")
-      client.wait_for_unit("network.target")
+      ${gopherClient}.wait_for_unit("network.target")
 
-      fileResponse = client.succeed("curl -f -s gopher://${gopherHost}//${fileName}")
+      fileResponse = ${gopherClient}.succeed("curl -f -s gopher://${gopherHost}/0/${fileName}")
 
       # the file response should return our created file exactly
-      if not (fileResponse == "${fileContent}\n"):
+      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 = client.succeed("curl -f -s gopher://${gopherHost}")
+      dirResponse = ${gopherClient}.succeed("curl -f -s gopher://${gopherHost}")
       dirEntries = [l[0] for l in dirResponse.split("\n") if len(l) > 0]
       dirEntries.sort()
 
diff --git a/nixos/tests/spike.nix b/nixos/tests/spike.nix
index cb89df73877..09035a15641 100644
--- a/nixos/tests/spike.nix
+++ b/nixos/tests/spike.nix
@@ -17,6 +17,6 @@ in
     ''
       machine.wait_for_unit("multi-user.target")
       output = machine.succeed("spike -m64 $(which pk) $(which hello)")
-      assert output == "Hello, world!\n"
+      assert "Hello, world!" in output
     '';
 })
diff --git a/nixos/tests/sway.nix b/nixos/tests/sway.nix
new file mode 100644
index 00000000000..fad7f7dc4e6
--- /dev/null
+++ b/nixos/tests/sway.nix
@@ -0,0 +1,92 @@
+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 ];
+      # Use a fixed SWAYSOCK path (for swaymsg):
+      variables."SWAYSOCK" = "/tmp/sway-ipc.sock";
+      # For convenience:
+      shellAliases = {
+        test-x11 = "glinfo | head -n 3 | 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";
+      };
+    };
+
+    # 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;
+
+    virtualisation.memorySize = 1024;
+    # Need to switch to a different VGA card / GPU driver than the default one (std) so that Sway can launch:
+    virtualisation.qemu.options = [ "-vga virtio" ];
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: ''
+    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:
+    machine.succeed(
+        "su - alice -c '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.screenshot("alacritty_glinfo")
+    machine.succeed("pkill alacritty")
+
+    # Start a terminal (Alacritty) on workspace 3:
+    machine.send_key("alt-3")
+    machine.succeed(
+        "su - alice -c 'swaymsg exec WINIT_UNIX_BACKEND=wayland DISPLAY=invalid alacritty'"
+    )
+    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.screenshot("alacritty_wayland_info")
+    machine.send_key("alt-shift-q")
+    machine.wait_until_fails("pgrep alacritty")
+
+    # Test swaynag:
+    machine.send_key("alt-shift-e")
+    machine.wait_for_text("You pressed the exit shortcut.")
+    machine.screenshot("sway_exit")
+
+    # Exit Sway and verify process exit status 0:
+    machine.succeed("su - alice -c 'swaymsg exit || true'")
+    machine.wait_for_file("/tmp/sway-exit-ok")
+  '';
+})
diff --git a/nixos/tests/systemd-confinement.nix b/nixos/tests/systemd-confinement.nix
index ebf6d218fd6..d04e4a3f867 100644
--- a/nixos/tests/systemd-confinement.nix
+++ b/nixos/tests/systemd-confinement.nix
@@ -150,6 +150,7 @@ import ./make-test-python.nix {
 
     config.users.groups.chroot-testgroup = {};
     config.users.users.chroot-testuser = {
+      isSystemUser = true;
       description = "Chroot Test User";
       group = "chroot-testgroup";
     };
diff --git a/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix b/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix
index 5831c8692f6..94f17605e00 100644
--- a/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix
+++ b/nixos/tests/systemd-networkd-ipv6-prefix-delegation.nix
@@ -43,7 +43,7 @@ import ./make-test-python.nix ({pkgs, ...}: {
       # Everyone on the "isp" machine will be able to add routes to the kernel.
       security.wrappers.add-dhcpd-lease = {
         source = pkgs.writeShellScript "add-dhcpd-lease" ''
-          exec ${pkgs.iproute}/bin/ip -6 route replace "$1" via "$2"
+          exec ${pkgs.iproute2}/bin/ip -6 route replace "$1" via "$2"
         '';
         capabilities = "cap_net_admin+ep";
       };
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/trafficserver.nix b/nixos/tests/trafficserver.nix
new file mode 100644
index 00000000000..3979a1b4a48
--- /dev/null
+++ b/nixos/tests/trafficserver.nix
@@ -0,0 +1,176 @@
+# 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)
+
+    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/turbovnc-headless-server.nix b/nixos/tests/turbovnc-headless-server.nix
new file mode 100644
index 00000000000..35da9a53d2d
--- /dev/null
+++ b/nixos/tests/turbovnc-headless-server.nix
@@ -0,0 +1,171 @@
+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
+            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 &",
+        )
+
+
+    # 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 &"
+        )
+        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 &"
+        )
+        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/unbound.nix b/nixos/tests/unbound.nix
index d4b8bb15ced..e24c3ef6c99 100644
--- a/nixos/tests/unbound.nix
+++ b/nixos/tests/unbound.nix
@@ -61,13 +61,16 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
 
         services.unbound = {
           enable = true;
-          interfaces = [ "192.168.0.1" "fd21::1" "::1" "127.0.0.1" ];
-          allowedAccess = [ "192.168.0.0/24" "fd21::/64" "::1" "127.0.0.0/8" ];
-          extraConfig = ''
-            server:
-              local-data: "example.local. IN A 1.2.3.4"
-              local-data: "example.local. IN AAAA abcd::eeff"
-          '';
+          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"''
+              ];
+            };
+          };
         };
       };
 
@@ -90,19 +93,25 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
 
         services.unbound = {
           enable = true;
-          allowedAccess = [ "192.168.0.0/24" "fd21::/64" "::1" "127.0.0.0/8" ];
-          interfaces = [ "::1" "127.0.0.1" "192.168.0.2" "fd21::2"
-                         "192.168.0.2@853" "fd21::2@853" "::1@853" "127.0.0.1@853"
-                         "192.168.0.2@443" "fd21::2@443" "::1@443" "127.0.0.1@443" ];
-          forwardAddresses = [
-            (lib.head nodes.authoritative.config.networking.interfaces.eth1.ipv6.addresses).address
-            (lib.head nodes.authoritative.config.networking.interfaces.eth1.ipv4.addresses).address
-          ];
-          extraConfig = ''
-            server:
-              tls-service-pem: ${cert}/cert.pem
-              tls-service-key: ${cert}/key.pem
-          '';
+          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
+                ];
+              }
+            ];
+          };
         };
       };
 
@@ -122,30 +131,36 @@ import ./make-test-python.nix ({ pkgs, lib, ... }:
 
         services.unbound = {
           enable = true;
-          allowedAccess = [ "::1" "127.0.0.0/8" ];
-          interfaces = [ "::1" "127.0.0.1" ];
+          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";
-          extraConfig = ''
-            include: "/etc/unbound/extra*.conf"
-          '';
         };
 
         users.users = {
           # user that is permitted to access the unix socket
-          someuser.extraGroups = [
-            config.users.users.unbound.group
-          ];
+          someuser = {
+            isSystemUser = true;
+            extraGroups = [
+              config.users.users.unbound.group
+            ];
+          };
 
           # user that is not permitted to access the unix socket
-          unauthorizeduser = {};
+          unauthorizeduser = { isSystemUser = true; };
         };
 
+        # 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}
+            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:
diff --git a/nixos/tests/wiki-js.nix b/nixos/tests/wiki-js.nix
new file mode 100644
index 00000000000..9aa87d15366
--- /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(
+            "set -o pipefail; 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(
+            "set -o pipefail; 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(
+            "set -o pipefail; 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/wireguard/basic.nix b/nixos/tests/wireguard/basic.nix
index a31e92e8649..36ab226cde0 100644
--- a/nixos/tests/wireguard/basic.nix
+++ b/nixos/tests/wireguard/basic.nix
@@ -52,9 +52,9 @@ import ../make-test-python.nix ({ pkgs, lib, ...} :
               inherit (wg-snakeoil-keys.peer0) publicKey;
             };
 
-            postSetup = let inherit (pkgs) iproute; in ''
-              ${iproute}/bin/ip route replace 10.23.42.1/32 dev wg0
-              ${iproute}/bin/ip route replace fc00::1/128 dev wg0
+            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
             '';
           };
         };
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/zigbee2mqtt.nix b/nixos/tests/zigbee2mqtt.nix
index b7bb21f9227..98aadbb699b 100644
--- a/nixos/tests/zigbee2mqtt.nix
+++ b/nixos/tests/zigbee2mqtt.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ pkgs, ... }:
+import ./make-test-python.nix ({ pkgs, lib, ... }:
 
   {
     machine = { pkgs, ... }:
@@ -6,6 +6,8 @@ import ./make-test-python.nix ({ pkgs, ... }:
         services.zigbee2mqtt = {
           enable = true;
         };
+
+        systemd.services.zigbee2mqtt.serviceConfig.DevicePolicy = lib.mkForce "auto";
       };
 
     testScript = ''
@@ -14,6 +16,8 @@ import ./make-test-python.nix ({ pkgs, ... }:
       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"))
     '';
   }
 )