about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/administration/declarative-containers.section.md2
-rw-r--r--nixos/doc/manual/configuration/config-file.section.md2
-rw-r--r--nixos/doc/manual/development/option-declarations.section.md13
-rw-r--r--nixos/doc/manual/from_md/administration/declarative-containers.section.xml2
-rw-r--r--nixos/doc/manual/from_md/configuration/config-file.section.xml2
-rw-r--r--nixos/doc/manual/from_md/development/option-declarations.section.xml16
-rw-r--r--nixos/doc/manual/from_md/installation/installing-usb.section.xml148
-rw-r--r--nixos/doc/manual/from_md/installation/installing.chapter.xml1089
-rw-r--r--nixos/doc/manual/from_md/installation/obtaining.chapter.xml29
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2111.section.xml2
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2205.section.xml27
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2211.section.xml547
-rw-r--r--nixos/doc/manual/installation/installing-usb.section.md93
-rw-r--r--nixos/doc/manual/installation/installing.chapter.md178
-rw-r--r--nixos/doc/manual/installation/obtaining.chapter.md19
-rwxr-xr-xnixos/doc/manual/md-to-db.sh1
-rw-r--r--nixos/doc/manual/release-notes/rl-2111.section.md2
-rw-r--r--nixos/doc/manual/release-notes/rl-2205.section.md14
-rw-r--r--nixos/doc/manual/release-notes/rl-2211.section.md154
-rw-r--r--nixos/lib/make-options-doc/default.nix11
-rw-r--r--nixos/lib/make-options-doc/generateCommonMark.py2
-rw-r--r--nixos/lib/make-options-doc/mergeJSON.py27
-rw-r--r--nixos/lib/systemd-lib.nix41
-rw-r--r--nixos/lib/systemd-unit-options.nix44
-rw-r--r--nixos/lib/testing-python.nix3
-rw-r--r--nixos/lib/utils.nix18
-rw-r--r--nixos/modules/config/console.nix18
-rw-r--r--nixos/modules/config/gtk/gtk-icon-cache.nix4
-rw-r--r--nixos/modules/config/power-management.nix2
-rw-r--r--nixos/modules/config/sysctl.nix16
-rw-r--r--nixos/modules/config/update-users-groups.pl4
-rw-r--r--nixos/modules/config/users-groups.nix2
-rw-r--r--nixos/modules/hardware/brillo.nix5
-rw-r--r--nixos/modules/hardware/ubertooth.nix2
-rw-r--r--nixos/modules/hardware/usb-storage.nix20
-rw-r--r--nixos/modules/hardware/video/nvidia.nix2
-rw-r--r--nixos/modules/hardware/wooting.nix2
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix5
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix2
-rw-r--r--nixos/modules/installer/sd-card/sd-image.nix15
-rw-r--r--nixos/modules/misc/documentation.nix8
-rw-r--r--nixos/modules/misc/ids.nix2
-rw-r--r--nixos/modules/misc/man-db.nix2
-rw-r--r--nixos/modules/misc/nixpkgs.nix11
-rw-r--r--nixos/modules/misc/nixpkgs/test.nix6
-rw-r--r--nixos/modules/module-list.nix23
-rw-r--r--nixos/modules/profiles/minimal.nix5
-rw-r--r--nixos/modules/programs/bash/bash.nix2
-rw-r--r--nixos/modules/programs/firefox.nix91
-rw-r--r--nixos/modules/programs/fish.nix2
-rw-r--r--nixos/modules/programs/kclock.nix2
-rw-r--r--nixos/modules/programs/less.nix3
-rw-r--r--nixos/modules/programs/mdevctl.nix18
-rw-r--r--nixos/modules/programs/mepo.nix46
-rw-r--r--nixos/modules/programs/tmux.nix19
-rw-r--r--nixos/modules/programs/zsh/zsh.nix10
-rw-r--r--nixos/modules/security/acme/default.nix12
-rw-r--r--nixos/modules/security/pam.nix64
-rw-r--r--nixos/modules/security/polkit.nix8
-rw-r--r--nixos/modules/services/audio/navidrome.nix2
-rw-r--r--nixos/modules/services/audio/ympd.nix2
-rw-r--r--nixos/modules/services/backup/bacula.nix6
-rw-r--r--nixos/modules/services/backup/btrbk.nix102
-rw-r--r--nixos/modules/services/backup/duplicati.nix2
-rw-r--r--nixos/modules/services/blockchain/ethereum/erigon.nix114
-rw-r--r--nixos/modules/services/blockchain/ethereum/lighthouse.nix313
-rw-r--r--nixos/modules/services/cluster/kubernetes/flannel.nix2
-rw-r--r--nixos/modules/services/cluster/kubernetes/kubelet.nix7
-rw-r--r--nixos/modules/services/computing/foldingathome/client.nix2
-rw-r--r--nixos/modules/services/continuous-integration/github-runner.nix401
-rw-r--r--nixos/modules/services/continuous-integration/github-runner/options.nix173
-rw-r--r--nixos/modules/services/continuous-integration/github-runner/service.nix257
-rw-r--r--nixos/modules/services/continuous-integration/github-runners.nix56
-rw-r--r--nixos/modules/services/continuous-integration/gocd-server/default.nix6
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/default.nix6
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/job-builder.nix5
-rw-r--r--nixos/modules/services/databases/couchdb.nix8
-rw-r--r--nixos/modules/services/databases/influxdb2.nix1
-rw-r--r--nixos/modules/services/databases/opentsdb.nix10
-rw-r--r--nixos/modules/services/databases/pgmanage.nix2
-rw-r--r--nixos/modules/services/databases/postgresql.xml33
-rw-r--r--nixos/modules/services/databases/redis.nix35
-rw-r--r--nixos/modules/services/desktops/geoclue2.nix1
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json28
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json38
-rw-r--r--nixos/modules/services/games/teeworlds.nix2
-rw-r--r--nixos/modules/services/hardware/pcscd.nix10
-rw-r--r--nixos/modules/services/hardware/sane.nix12
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix3
-rw-r--r--nixos/modules/services/hardware/udev.nix6
-rw-r--r--nixos/modules/services/hardware/udisks2.nix7
-rw-r--r--nixos/modules/services/home-automation/home-assistant.nix6
-rw-r--r--nixos/modules/services/home-automation/zigbee2mqtt.nix2
-rw-r--r--nixos/modules/services/logging/fluentd.nix6
-rw-r--r--nixos/modules/services/logging/journalwatch.nix2
-rw-r--r--nixos/modules/services/logging/logcheck.nix8
-rw-r--r--nixos/modules/services/logging/logrotate.nix202
-rw-r--r--nixos/modules/services/mail/listmonk.nix2
-rw-r--r--nixos/modules/services/mail/mailman.nix16
-rw-r--r--nixos/modules/services/mail/rss2email.nix3
-rw-r--r--nixos/modules/services/matrix/appservice-discord.nix2
-rw-r--r--nixos/modules/services/matrix/mautrix-facebook.nix1
-rw-r--r--nixos/modules/services/matrix/mautrix-telegram.nix20
-rw-r--r--nixos/modules/services/misc/airsonic.nix2
-rw-r--r--nixos/modules/services/misc/ankisyncd.nix2
-rw-r--r--nixos/modules/services/misc/apache-kafka.nix2
-rw-r--r--nixos/modules/services/misc/ethminer.nix117
-rw-r--r--nixos/modules/services/misc/exhibitor.nix11
-rw-r--r--nixos/modules/services/misc/geoipupdate.nix4
-rw-r--r--nixos/modules/services/misc/gitea.nix2
-rw-r--r--nixos/modules/services/misc/gitlab.nix11
-rw-r--r--nixos/modules/services/misc/gogs.nix4
-rw-r--r--nixos/modules/services/misc/gollum.nix8
-rw-r--r--nixos/modules/services/misc/languagetool.nix2
-rw-r--r--nixos/modules/services/misc/mx-puppet-discord.nix2
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix19
-rw-r--r--nixos/modules/services/misc/ntfy-sh.nix100
-rw-r--r--nixos/modules/services/misc/parsoid.nix2
-rw-r--r--nixos/modules/services/misc/podgrab.nix2
-rw-r--r--nixos/modules/services/misc/portunus.nix4
-rw-r--r--nixos/modules/services/misc/pykms.nix2
-rw-r--r--nixos/modules/services/misc/redmine.nix2
-rw-r--r--nixos/modules/services/misc/rippled.nix2
-rw-r--r--nixos/modules/services/misc/rmfakecloud.nix2
-rw-r--r--nixos/modules/services/misc/sonarr.nix11
-rw-r--r--nixos/modules/services/misc/sourcehut/default.nix2
-rw-r--r--nixos/modules/services/misc/synergy.nix2
-rw-r--r--nixos/modules/services/misc/tautulli.nix2
-rw-r--r--nixos/modules/services/misc/zoneminder.nix2
-rw-r--r--nixos/modules/services/misc/zookeeper.nix8
-rw-r--r--nixos/modules/services/monitoring/alerta.nix2
-rw-r--r--nixos/modules/services/monitoring/arbtt.nix8
-rw-r--r--nixos/modules/services/monitoring/bosun.nix8
-rw-r--r--nixos/modules/services/monitoring/cadvisor.nix8
-rw-r--r--nixos/modules/services/monitoring/datadog-agent.nix8
-rw-r--r--nixos/modules/services/monitoring/grafana-image-renderer.nix6
-rw-r--r--nixos/modules/services/monitoring/grafana-reporter.nix4
-rw-r--r--nixos/modules/services/monitoring/grafana.nix1563
-rw-r--r--nixos/modules/services/monitoring/heapster.nix6
-rw-r--r--nixos/modules/services/monitoring/karma.nix128
-rw-r--r--nixos/modules/services/monitoring/parsedmarc.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/default.nix35
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix3
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/kea.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/mail.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/node.nix8
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix7
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/tor.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/zfs.nix44
-rw-r--r--nixos/modules/services/monitoring/riemann.nix9
-rw-r--r--nixos/modules/services/monitoring/smartd.nix3
-rw-r--r--nixos/modules/services/monitoring/teamviewer.nix2
-rw-r--r--nixos/modules/services/monitoring/uptime-kuma.nix76
-rw-r--r--nixos/modules/services/monitoring/vmagent.nix100
-rw-r--r--nixos/modules/services/monitoring/zabbix-server.nix2
-rw-r--r--nixos/modules/services/network-filesystems/kubo.nix98
-rw-r--r--nixos/modules/services/network-filesystems/litestream/litestream.xml2
-rw-r--r--nixos/modules/services/network-filesystems/samba-wsdd.nix2
-rw-r--r--nixos/modules/services/networking/adguardhome.nix82
-rw-r--r--nixos/modules/services/networking/bitcoind.nix2
-rw-r--r--nixos/modules/services/networking/bitlbee.nix2
-rw-r--r--nixos/modules/services/networking/blocky.nix2
-rw-r--r--nixos/modules/services/networking/chisel-server.nix99
-rw-r--r--nixos/modules/services/networking/consul.nix2
-rw-r--r--nixos/modules/services/networking/croc.nix2
-rw-r--r--nixos/modules/services/networking/dnscrypt-proxy2.nix1
-rw-r--r--nixos/modules/services/networking/dnscrypt-wrapper.nix4
-rw-r--r--nixos/modules/services/networking/eternal-terminal.nix2
-rw-r--r--nixos/modules/services/networking/firefox-syncserver.nix90
-rw-r--r--nixos/modules/services/networking/flannel.nix6
-rw-r--r--nixos/modules/services/networking/hostapd.nix2
-rw-r--r--nixos/modules/services/networking/jibri/default.nix2
-rw-r--r--nixos/modules/services/networking/jitsi-videobridge.nix2
-rw-r--r--nixos/modules/services/networking/magic-wormhole-mailbox-server.nix2
-rw-r--r--nixos/modules/services/networking/mosquitto.nix11
-rw-r--r--nixos/modules/services/networking/mozillavpn.nix9
-rw-r--r--nixos/modules/services/networking/mullvad-vpn.nix48
-rw-r--r--nixos/modules/services/networking/nats.nix2
-rw-r--r--nixos/modules/services/networking/networkmanager.nix2
-rw-r--r--nixos/modules/services/networking/nntp-proxy.nix4
-rw-r--r--nixos/modules/services/networking/ntp/chrony.nix4
-rw-r--r--nixos/modules/services/networking/ntp/ntpd.nix4
-rw-r--r--nixos/modules/services/networking/onedrive.nix6
-rw-r--r--nixos/modules/services/networking/ostinato.nix2
-rw-r--r--nixos/modules/services/networking/owamp.nix2
-rw-r--r--nixos/modules/services/networking/polipo.nix8
-rw-r--r--nixos/modules/services/networking/shadowsocks.nix2
-rw-r--r--nixos/modules/services/networking/smokeping.nix20
-rw-r--r--nixos/modules/services/networking/snowflake-proxy.nix2
-rw-r--r--nixos/modules/services/networking/sslh.nix2
-rw-r--r--nixos/modules/services/networking/syncthing.nix17
-rw-r--r--nixos/modules/services/networking/teleport.nix2
-rw-r--r--nixos/modules/services/networking/tox-bootstrapd.nix2
-rw-r--r--nixos/modules/services/networking/toxvpn.nix2
-rw-r--r--nixos/modules/services/networking/vdirsyncer.nix214
-rw-r--r--nixos/modules/services/networking/wireguard.nix18
-rw-r--r--nixos/modules/services/networking/xray.nix96
-rw-r--r--nixos/modules/services/networking/xrdp.nix2
-rw-r--r--nixos/modules/services/networking/yggdrasil.nix2
-rw-r--r--nixos/modules/services/networking/zerotierone.nix2
-rw-r--r--nixos/modules/services/printing/cupsd.nix14
-rw-r--r--nixos/modules/services/search/hound.nix1
-rw-r--r--nixos/modules/services/search/solr.nix2
-rw-r--r--nixos/modules/services/security/endlessh-go.nix2
-rw-r--r--nixos/modules/services/security/endlessh.nix99
-rw-r--r--nixos/modules/services/security/tor.nix4
-rw-r--r--nixos/modules/services/system/automatic-timezoned.nix92
-rw-r--r--nixos/modules/services/system/cloud-init.nix3
-rw-r--r--nixos/modules/services/system/nscd.nix38
-rw-r--r--nixos/modules/services/torrent/transmission.nix2
-rw-r--r--nixos/modules/services/ttys/getty.nix2
-rw-r--r--nixos/modules/services/web-apps/alps.nix2
-rw-r--r--nixos/modules/services/web-apps/atlassian/crowd.nix4
-rw-r--r--nixos/modules/services/web-apps/bookstack.nix9
-rw-r--r--nixos/modules/services/web-apps/changedetection-io.nix219
-rw-r--r--nixos/modules/services/web-apps/dex.nix4
-rw-r--r--nixos/modules/services/web-apps/discourse.nix11
-rw-r--r--nixos/modules/services/web-apps/dokuwiki.nix25
-rw-r--r--nixos/modules/services/web-apps/freshrss.nix20
-rw-r--r--nixos/modules/services/web-apps/galene.nix2
-rw-r--r--nixos/modules/services/web-apps/healthchecks.nix14
-rw-r--r--nixos/modules/services/web-apps/invoiceplane.nix65
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix2
-rw-r--r--nixos/modules/services/web-apps/mastodon.nix89
-rw-r--r--nixos/modules/services/web-apps/matomo.nix8
-rw-r--r--nixos/modules/services/web-apps/miniflux.nix2
-rw-r--r--nixos/modules/services/web-apps/netbox.nix2
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix34
-rw-r--r--nixos/modules/services/web-apps/nextcloud.xml2
-rw-r--r--nixos/modules/services/web-apps/peertube.nix322
-rw-r--r--nixos/modules/services/web-apps/prosody-filer.nix2
-rw-r--r--nixos/modules/services/web-apps/restya-board.nix4
-rw-r--r--nixos/modules/services/web-apps/shiori.nix2
-rw-r--r--nixos/modules/services/web-apps/snipe-it.nix57
-rw-r--r--nixos/modules/services/web-apps/trilium.nix2
-rw-r--r--nixos/modules/services/web-apps/tt-rss.nix2
-rw-r--r--nixos/modules/services/web-apps/wordpress.nix2
-rw-r--r--nixos/modules/services/web-apps/youtrack.nix2
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix16
-rw-r--r--nixos/modules/services/web-servers/caddy/default.nix3
-rw-r--r--nixos/modules/services/web-servers/garage.nix91
-rw-r--r--nixos/modules/services/web-servers/merecat.nix55
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix31
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix8
-rw-r--r--nixos/modules/services/x11/clight.nix8
-rw-r--r--nixos/modules/services/x11/desktop-managers/cinnamon.nix42
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/mobile.nix26
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix1
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix15
-rw-r--r--nixos/modules/services/x11/gdk-pixbuf.nix29
-rw-r--r--nixos/modules/services/x11/picom.nix14
-rw-r--r--nixos/modules/services/x11/window-managers/default.nix1
-rw-r--r--nixos/modules/services/x11/window-managers/hypr.nix25
-rw-r--r--nixos/modules/system/activation/specialisation.nix85
-rw-r--r--nixos/modules/system/activation/top-level.nix105
-rw-r--r--nixos/modules/system/boot/binfmt.nix1
-rw-r--r--nixos/modules/system/boot/initrd-ssh.nix2
-rw-r--r--nixos/modules/system/boot/kernel.nix5
-rw-r--r--nixos/modules/system/boot/loader/grub/grub.nix5
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix24
-rw-r--r--nixos/modules/system/boot/luksroot.nix6
-rw-r--r--nixos/modules/system/boot/networkd.nix2
-rw-r--r--nixos/modules/system/boot/systemd.nix6
-rw-r--r--nixos/modules/system/boot/systemd/initrd.nix16
-rw-r--r--nixos/modules/system/boot/systemd/logind.nix2
-rw-r--r--nixos/modules/system/boot/systemd/nspawn.nix4
-rw-r--r--nixos/modules/system/boot/systemd/tmpfiles.nix1
-rw-r--r--nixos/modules/system/boot/uvesafb.nix39
-rw-r--r--nixos/modules/system/etc/setup-etc.pl2
-rw-r--r--nixos/modules/tasks/filesystems.nix2
-rw-r--r--nixos/modules/tasks/filesystems/zfs.nix13
-rw-r--r--nixos/modules/tasks/network-interfaces.nix26
-rw-r--r--nixos/modules/virtualisation/container-config.nix8
-rw-r--r--nixos/modules/virtualisation/libvirtd.nix1
-rw-r--r--nixos/modules/virtualisation/nixos-containers.nix24
-rw-r--r--nixos/modules/virtualisation/vmware-guest.nix3
-rw-r--r--nixos/release-combined.nix1
-rw-r--r--nixos/release-small.nix2
-rw-r--r--nixos/tests/adguardhome.nix20
-rw-r--r--nixos/tests/all-tests.nix23
-rw-r--r--nixos/tests/bazarr.nix6
-rw-r--r--nixos/tests/btrbk-section-order.nix51
-rw-r--r--nixos/tests/chromium.nix11
-rw-r--r--nixos/tests/cloud-init-hostname.nix46
-rw-r--r--nixos/tests/cloud-init.nix20
-rw-r--r--nixos/tests/containers-unified-hierarchy.nix21
-rw-r--r--nixos/tests/dhparams.nix138
-rw-r--r--nixos/tests/dnscrypt-proxy2.nix6
-rw-r--r--nixos/tests/endlessh.nix43
-rw-r--r--nixos/tests/fscrypt.nix50
-rw-r--r--nixos/tests/garage.nix169
-rw-r--r--nixos/tests/grafana/basic.nix (renamed from nixos/tests/grafana.nix)35
-rw-r--r--nixos/tests/grafana/default.nix9
-rw-r--r--nixos/tests/grafana/provision/contact-points.yaml9
-rw-r--r--nixos/tests/grafana/provision/dashboards.yaml6
-rw-r--r--nixos/tests/grafana/provision/datasources.yaml7
-rw-r--r--nixos/tests/grafana/provision/default.nix229
-rw-r--r--nixos/tests/grafana/provision/mute-timings.yaml4
-rw-r--r--nixos/tests/grafana/provision/policies.yaml4
-rw-r--r--nixos/tests/grafana/provision/rules.yaml36
-rw-r--r--nixos/tests/grafana/provision/templates.yaml5
-rw-r--r--nixos/tests/grafana/provision/test_dashboard.json47
-rw-r--r--nixos/tests/installed-tests/default.nix4
-rw-r--r--nixos/tests/installed-tests/flatpak-builder.nix2
-rw-r--r--nixos/tests/installed-tests/flatpak.nix2
-rw-r--r--nixos/tests/installed-tests/gdk-pixbuf.nix2
-rw-r--r--nixos/tests/invoiceplane.nix16
-rw-r--r--nixos/tests/jenkins.nix2
-rw-r--r--nixos/tests/jibri.nix3
-rw-r--r--nixos/tests/k3s/multi-node.nix48
-rw-r--r--nixos/tests/k3s/single-node.nix17
-rw-r--r--nixos/tests/kafka.nix17
-rw-r--r--nixos/tests/karma.nix84
-rw-r--r--nixos/tests/kernel-generic.nix2
-rw-r--r--nixos/tests/kubo.nix4
-rw-r--r--nixos/tests/libvirtd.nix9
-rw-r--r--nixos/tests/logrotate.nix29
-rw-r--r--nixos/tests/lxd-image-server.nix4
-rw-r--r--nixos/tests/lxd.nix6
-rw-r--r--nixos/tests/merecat.nix28
-rw-r--r--nixos/tests/mosquitto.nix12
-rw-r--r--nixos/tests/mysql/common.nix7
-rw-r--r--nixos/tests/nextcloud/default.nix2
-rw-r--r--nixos/tests/nginx-njs.nix27
-rw-r--r--nixos/tests/nscd.nix38
-rw-r--r--nixos/tests/ntfy-sh.nix20
-rw-r--r--nixos/tests/podman/default.nix73
-rw-r--r--nixos/tests/printing.nix1
-rw-r--r--nixos/tests/prometheus-exporters.nix39
-rw-r--r--nixos/tests/quake3.nix95
-rw-r--r--nixos/tests/shadow.nix30
-rw-r--r--nixos/tests/smokeping.nix2
-rw-r--r--nixos/tests/systemd-initrd-luks-fido2.nix45
-rw-r--r--nixos/tests/systemd-initrd-luks-tpm2.nix72
-rw-r--r--nixos/tests/systemd-machinectl.nix14
-rw-r--r--nixos/tests/systemd-no-tainted.nix14
-rw-r--r--nixos/tests/systemd-portabled.nix51
-rw-r--r--nixos/tests/tracee.nix11
-rw-r--r--nixos/tests/upnp.nix4
-rw-r--r--nixos/tests/uptime-kuma.nix19
-rw-r--r--nixos/tests/web-apps/healthchecks.nix6
-rw-r--r--nixos/tests/web-apps/mastodon.nix8
-rw-r--r--nixos/tests/wrappers.nix79
-rw-r--r--nixos/tests/zrepl.nix4
345 files changed, 8978 insertions, 2986 deletions
diff --git a/nixos/doc/manual/administration/declarative-containers.section.md b/nixos/doc/manual/administration/declarative-containers.section.md
index 00fd244bb91fb..eaa50d3c663d4 100644
--- a/nixos/doc/manual/administration/declarative-containers.section.md
+++ b/nixos/doc/manual/administration/declarative-containers.section.md
@@ -9,7 +9,7 @@ containers.database =
   { config =
       { config, pkgs, ... }:
       { services.postgresql.enable = true;
-      services.postgresql.package = pkgs.postgresql_10;
+      services.postgresql.package = pkgs.postgresql_14;
       };
   };
 ```
diff --git a/nixos/doc/manual/configuration/config-file.section.md b/nixos/doc/manual/configuration/config-file.section.md
index f21ba113bf8c6..efd231fd1f4e4 100644
--- a/nixos/doc/manual/configuration/config-file.section.md
+++ b/nixos/doc/manual/configuration/config-file.section.md
@@ -166,7 +166,7 @@ Packages
         pkgs.emacs
       ];
 
-    services.postgresql.package = pkgs.postgresql_10;
+    services.postgresql.package = pkgs.postgresql_14;
     ```
 
     The latter option definition changes the default PostgreSQL package
diff --git a/nixos/doc/manual/development/option-declarations.section.md b/nixos/doc/manual/development/option-declarations.section.md
index 0f5673dd4d585..88617ab1920a9 100644
--- a/nixos/doc/manual/development/option-declarations.section.md
+++ b/nixos/doc/manual/development/option-declarations.section.md
@@ -11,7 +11,7 @@ options = {
     type = type specification;
     default = default value;
     example = example value;
-    description = "Description for use in the NixOS manual.";
+    description = lib.mdDoc "Description for use in the NixOS manual.";
   };
 };
 ```
@@ -59,8 +59,9 @@ The function `mkOption` accepts the following arguments.
 :   A textual description of the option, in [Nixpkgs-flavored Markdown](
     https://nixos.org/nixpkgs/manual/#sec-contributing-markup) format, that will be
     included in the NixOS manual. During the migration process from DocBook
-    to CommonMark the description may also be written in DocBook, but this is
-    discouraged.
+    it is necessary to mark descriptions written in CommonMark with `lib.mdDoc`.
+    The description may still be written in DocBook (without any marker), but this
+    is discouraged and will be deprecated in the future.
 
 ## Utility functions for common option patterns {#sec-option-declarations-util}
 
@@ -83,7 +84,7 @@ lib.mkOption {
   type = lib.types.bool;
   default = false;
   example = true;
-  description = "Whether to enable magic.";
+  description = lib.mdDoc "Whether to enable magic.";
 }
 ```
 
@@ -116,7 +117,7 @@ lib.mkOption {
   type = lib.types.package;
   default = pkgs.hello;
   defaultText = lib.literalExpression "pkgs.hello";
-  description = "The hello package to use.";
+  description = lib.mdDoc "The hello package to use.";
 }
 ```
 
@@ -132,7 +133,7 @@ lib.mkOption {
   default = pkgs.ghc;
   defaultText = lib.literalExpression "pkgs.ghc";
   example = lib.literalExpression "pkgs.haskell.packages.ghc92.ghc.withPackages (hkgs: [ hkgs.primes ])";
-  description = "The GHC package to use.";
+  description = lib.mdDoc "The GHC package to use.";
 }
 ```
 
diff --git a/nixos/doc/manual/from_md/administration/declarative-containers.section.xml b/nixos/doc/manual/from_md/administration/declarative-containers.section.xml
index b8179dca1f8bd..4831c9c74e848 100644
--- a/nixos/doc/manual/from_md/administration/declarative-containers.section.xml
+++ b/nixos/doc/manual/from_md/administration/declarative-containers.section.xml
@@ -11,7 +11,7 @@ containers.database =
   { config =
       { config, pkgs, ... }:
       { services.postgresql.enable = true;
-      services.postgresql.package = pkgs.postgresql_10;
+      services.postgresql.package = pkgs.postgresql_14;
       };
   };
 </programlisting>
diff --git a/nixos/doc/manual/from_md/configuration/config-file.section.xml b/nixos/doc/manual/from_md/configuration/config-file.section.xml
index 952c6e6003021..9792116eb08d5 100644
--- a/nixos/doc/manual/from_md/configuration/config-file.section.xml
+++ b/nixos/doc/manual/from_md/configuration/config-file.section.xml
@@ -217,7 +217,7 @@ environment.systemPackages =
     pkgs.emacs
   ];
 
-services.postgresql.package = pkgs.postgresql_10;
+services.postgresql.package = pkgs.postgresql_14;
 </programlisting>
         <para>
           The latter option definition changes the default PostgreSQL
diff --git a/nixos/doc/manual/from_md/development/option-declarations.section.xml b/nixos/doc/manual/from_md/development/option-declarations.section.xml
index 69163853b6279..0932a51a18cdb 100644
--- a/nixos/doc/manual/from_md/development/option-declarations.section.xml
+++ b/nixos/doc/manual/from_md/development/option-declarations.section.xml
@@ -12,7 +12,7 @@ options = {
     type = type specification;
     default = default value;
     example = example value;
-    description = &quot;Description for use in the NixOS manual.&quot;;
+    description = lib.mdDoc &quot;Description for use in the NixOS manual.&quot;;
   };
 };
 </programlisting>
@@ -98,9 +98,11 @@ options = {
           A textual description of the option, in
           <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-contributing-markup">Nixpkgs-flavored
           Markdown</link> format, that will be included in the NixOS
-          manual. During the migration process from DocBook to
-          CommonMark the description may also be written in DocBook, but
-          this is discouraged.
+          manual. During the migration process from DocBook it is
+          necessary to mark descriptions written in CommonMark with
+          <literal>lib.mdDoc</literal>. The description may still be
+          written in DocBook (without any marker), but this is
+          discouraged and will be deprecated in the future.
         </para>
       </listitem>
     </varlistentry>
@@ -132,7 +134,7 @@ lib.mkOption {
   type = lib.types.bool;
   default = false;
   example = true;
-  description = &quot;Whether to enable magic.&quot;;
+  description = lib.mdDoc &quot;Whether to enable magic.&quot;;
 }
 </programlisting>
       <section xml:id="sec-option-declarations-util-mkPackageOption">
@@ -182,7 +184,7 @@ lib.mkOption {
   type = lib.types.package;
   default = pkgs.hello;
   defaultText = lib.literalExpression &quot;pkgs.hello&quot;;
-  description = &quot;The hello package to use.&quot;;
+  description = lib.mdDoc &quot;The hello package to use.&quot;;
 }
 </programlisting>
         <anchor xml:id="ex-options-declarations-util-mkPackageOption-ghc" />
@@ -197,7 +199,7 @@ lib.mkOption {
   default = pkgs.ghc;
   defaultText = lib.literalExpression &quot;pkgs.ghc&quot;;
   example = lib.literalExpression &quot;pkgs.haskell.packages.ghc92.ghc.withPackages (hkgs: [ hkgs.primes ])&quot;;
-  description = &quot;The GHC package to use.&quot;;
+  description = lib.mdDoc &quot;The GHC package to use.&quot;;
 }
 </programlisting>
         <section xml:id="sec-option-declarations-eot">
diff --git a/nixos/doc/manual/from_md/installation/installing-usb.section.xml b/nixos/doc/manual/from_md/installation/installing-usb.section.xml
index df266eb16800f..9d12ac45aac21 100644
--- a/nixos/doc/manual/from_md/installation/installing-usb.section.xml
+++ b/nixos/doc/manual/from_md/installation/installing-usb.section.xml
@@ -1,35 +1,135 @@
 <section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-booting-from-usb">
-  <title>Booting from a USB Drive</title>
+  <title>Booting from a USB flash drive</title>
   <para>
-    For systems without CD drive, the NixOS live CD can be booted from a
-    USB stick. You can use the <literal>dd</literal> utility to write
-    the image: <literal>dd if=path-to-image of=/dev/sdX</literal>. Be
-    careful about specifying the correct drive; you can use the
-    <literal>lsblk</literal> command to get a list of block devices.
+    The image has to be written verbatim to the USB flash drive for it
+    to be bootable on UEFI and BIOS systems. Here are the recommended
+    tools to do that.
   </para>
-  <note>
-    <title>On macOS</title>
+  <section xml:id="sec-booting-from-usb-graphical">
+    <title>Creating bootable USB flash drive with a graphical
+    tool</title>
+    <para>
+      Etcher is a popular and user-friendly tool. It works on Linux,
+      Windows and macOS.
+    </para>
+    <para>
+      Download it from
+      <link xlink:href="https://www.balena.io/etcher/">balena.io</link>,
+      start the program, select the downloaded NixOS ISO, then select
+      the USB flash drive and flash it.
+    </para>
+    <warning>
+      <para>
+        Etcher reports errors and usage statistics by default, which can
+        be disabled in the settings.
+      </para>
+    </warning>
+    <para>
+      An alternative is
+      <link xlink:href="https://bztsrc.gitlab.io/usbimager">USBImager</link>,
+      which is very simple and does not connect to the internet.
+      Download the version with write-only (wo) interface for your
+      system. Start the program, select the image, select the USB flash
+      drive and click <quote>Write</quote>.
+    </para>
+  </section>
+  <section xml:id="sec-booting-from-usb-linux">
+    <title>Creating bootable USB flash drive from a Terminal on
+    Linux</title>
+    <orderedlist numeration="arabic" spacing="compact">
+      <listitem>
+        <para>
+          Plug in the USB flash drive.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Find the corresponding device with <literal>lsblk</literal>.
+          You can distinguish them by their size.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Make sure all partitions on the device are properly unmounted.
+          Replace <literal>sdX</literal> with your device (e.g.
+          <literal>sdb</literal>).
+        </para>
+      </listitem>
+    </orderedlist>
+    <programlisting>
+sudo umount /dev/sdX*
+</programlisting>
+    <orderedlist numeration="arabic" spacing="compact">
+      <listitem override="4">
+        <para>
+          Then use the <literal>dd</literal> utility to write the image
+          to the USB flash drive.
+        </para>
+      </listitem>
+    </orderedlist>
     <programlisting>
-$ diskutil list
-[..]
-/dev/diskN (external, physical):
-   #:                       TYPE NAME                    SIZE       IDENTIFIER
-[..]
-$ diskutil unmountDisk diskN
-Unmount of all volumes on diskN was successful
-$ sudo dd if=nix.iso of=/dev/rdiskN bs=1M
+sudo dd if=&lt;path-to-image&gt; of=/dev/sdX bs=4M conv=fsync
+</programlisting>
+  </section>
+  <section xml:id="sec-booting-from-usb-macos">
+    <title>Creating bootable USB flash drive from a Terminal on
+    macOS</title>
+    <orderedlist numeration="arabic" spacing="compact">
+      <listitem>
+        <para>
+          Plug in the USB flash drive.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Find the corresponding device with
+          <literal>diskutil list</literal>. You can distinguish them by
+          their size.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Make sure all partitions on the device are properly unmounted.
+          Replace <literal>diskX</literal> with your device (e.g.
+          <literal>disk1</literal>).
+        </para>
+      </listitem>
+    </orderedlist>
+    <programlisting>
+diskutil unmountDisk diskX
+</programlisting>
+    <orderedlist numeration="arabic" spacing="compact">
+      <listitem override="4">
+        <para>
+          Then use the <literal>dd</literal> utility to write the image
+          to the USB flash drive.
+        </para>
+      </listitem>
+    </orderedlist>
+    <programlisting>
+sudo dd if=&lt;path-to-image&gt; of=/dev/rdiskX bs=4m
 </programlisting>
     <para>
-      Using the 'raw' <literal>rdiskN</literal> device instead of
-      <literal>diskN</literal> completes in minutes instead of hours.
       After <literal>dd</literal> completes, a GUI dialog &quot;The disk
       you inserted was not readable by this computer&quot; will pop up,
       which can be ignored.
     </para>
-  </note>
-  <para>
-    The <literal>dd</literal> utility will write the image verbatim to
-    the drive, making it the recommended option for both UEFI and
-    non-UEFI installations.
-  </para>
+    <note>
+      <para>
+        Using the 'raw' <literal>rdiskX</literal> device instead of
+        <literal>diskX</literal> with dd completes in minutes instead of
+        hours.
+      </para>
+    </note>
+    <orderedlist numeration="arabic" spacing="compact">
+      <listitem override="5">
+        <para>
+          Eject the disk when it is finished.
+        </para>
+      </listitem>
+    </orderedlist>
+    <programlisting>
+diskutil eject /dev/diskX
+</programlisting>
+  </section>
 </section>
diff --git a/nixos/doc/manual/from_md/installation/installing.chapter.xml b/nixos/doc/manual/from_md/installation/installing.chapter.xml
index e0ff368b800c1..f2ed58c0c1fe5 100644
--- a/nixos/doc/manual/from_md/installation/installing.chapter.xml
+++ b/nixos/doc/manual/from_md/installation/installing.chapter.xml
@@ -1,26 +1,212 @@
 <chapter xmlns="http://docbook.org/ns/docbook"  xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude" xml:id="sec-installation">
   <title>Installing NixOS</title>
   <section xml:id="sec-installation-booting">
-    <title>Booting the system</title>
+    <title>Booting from the install medium</title>
     <para>
-      NixOS can be installed on BIOS or UEFI systems. The procedure for
-      a UEFI installation is by and large the same as a BIOS
-      installation. The differences are mentioned in the steps that
-      follow.
+      To begin the installation, you have to boot your computer from the
+      install drive.
     </para>
+    <orderedlist numeration="arabic">
+      <listitem>
+        <para>
+          Plug in the install drive. Then turn on or restart your
+          computer.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Open the boot menu by pressing the appropriate key, which is
+          usually shown on the display on early boot. Select the USB
+          flash drive (the option usually contains the word
+          <quote>USB</quote>). If you choose the incorrect drive, your
+          computer will likely continue to boot as normal. In that case
+          restart your computer and pick a different drive.
+        </para>
+        <note>
+          <para>
+            The key to open the boot menu is different across computer
+            brands and even models. It can be <keycap>F12</keycap>, but
+            also <keycap>F1</keycap>, <keycap>F9</keycap>,
+            <keycap>F10</keycap>, <keycap>Enter</keycap>,
+            <keycap>Del</keycap>, <keycap>Esc</keycap> or another
+            function key. If you are unsure and don’t see it on the
+            early boot screen, you can search online for your computers
+            brand, model followed by <quote>boot from usb</quote>. The
+            computer might not even have that feature, so you have to go
+            into the BIOS/UEFI settings to change the boot order. Again,
+            search online for details about your specific computer
+            model.
+          </para>
+          <para>
+            For Apple computers with Intel processors press and hold the
+            <keycap>⌥</keycap> (Option or Alt) key until you see the
+            boot menu. On Apple silicon press and hold the power button.
+          </para>
+        </note>
+        <note>
+          <para>
+            If your computer supports both BIOS and UEFI boot, choose
+            the UEFI option.
+          </para>
+        </note>
+        <note>
+          <para>
+            If you use a CD for the installation, the computer will
+            probably boot from it automatically. If not, choose the
+            option containing the word <quote>CD</quote> from the boot
+            menu.
+          </para>
+        </note>
+      </listitem>
+      <listitem>
+        <para>
+          Shortly after selecting the appropriate boot drive, you should
+          be presented with a menu with different installer options.
+          Leave the default and wait (or press <keycap>Enter</keycap> to
+          speed up).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The graphical images will start their corresponding desktop
+          environment and the graphical installer, which can take some
+          time. The minimal images will boot to a command line. You have
+          to follow the instructions in
+          <xref linkend="sec-installation-manual" /> there.
+        </para>
+      </listitem>
+    </orderedlist>
+  </section>
+  <section xml:id="sec-installation-graphical">
+    <title>Graphical Installation</title>
     <para>
-      The installation media can be burned to a CD, or now more
-      commonly, <quote>burned</quote> to a USB drive (see
-      <xref linkend="sec-booting-from-usb" />).
+      The graphical installer is recommended for desktop users and will
+      guide you through the installation.
     </para>
+    <orderedlist numeration="arabic">
+      <listitem>
+        <para>
+          In the <quote>Welcome</quote> screen, you can select the
+          language of the Installer and the installed system.
+        </para>
+        <tip>
+          <para>
+            Leaving the language as <quote>American English</quote> will
+            make it easier to search for error messages in a search
+            engine or to report an issue.
+          </para>
+        </tip>
+      </listitem>
+      <listitem>
+        <para>
+          Next you should choose your location to have the timezone set
+          correctly. You can actually click on the map!
+        </para>
+        <note>
+          <para>
+            The installer will use an online service to guess your
+            location based on your public IP address.
+          </para>
+        </note>
+      </listitem>
+      <listitem>
+        <para>
+          Then you can select the keyboard layout. The default keyboard
+          model should work well with most desktop keyboards. If you
+          have a special keyboard or notebook, your model might be in
+          the list. Select the language you are most comfortable typing
+          in.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          On the <quote>Users</quote> screen, you have to type in your
+          display name, login name and password. You can also enable an
+          option to automatically login to the desktop.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Then you have the option to choose a desktop environment. If
+          you want to create a custom setup with a window manager, you
+          can select <quote>No desktop</quote>.
+        </para>
+        <tip>
+          <para>
+            If you don’t have a favorite desktop and don’t know which
+            one to choose, you can stick to either GNOME or Plasma. They
+            have a quite different design, so you should choose
+            whichever you like better. They are both popular choices and
+            well tested on NixOS.
+          </para>
+        </tip>
+      </listitem>
+      <listitem>
+        <para>
+          You have the option to allow unfree software in the next
+          screen.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The easiest option in the <quote>Partitioning</quote> screen
+          is <quote>Erase disk</quote>, which will delete all data from
+          the selected disk and install the system on it. Also select
+          <quote>Swap (with Hibernation)</quote> in the dropdown below
+          it. You have the option to encrypt the whole disk with LUKS.
+        </para>
+        <note>
+          <para>
+            At the top left you see if the Installer was booted with
+            BIOS or UEFI. If you know your system supports UEFI and it
+            shows <quote>BIOS</quote>, reboot with the correct option.
+          </para>
+        </note>
+        <warning>
+          <para>
+            Make sure you have selected the correct disk at the top and
+            that no valuable data is still on the disk! It will be
+            deleted when formatting the disk.
+          </para>
+        </warning>
+      </listitem>
+      <listitem>
+        <para>
+          Check the choices you made in the <quote>Summary</quote> and
+          click <quote>Install</quote>.
+        </para>
+        <note>
+          <para>
+            The installation takes about 15 minutes. The time varies
+            based on the selected desktop environment, internet
+            connection speed and disk write speed.
+          </para>
+        </note>
+      </listitem>
+      <listitem>
+        <para>
+          When the install is complete, remove the USB flash drive and
+          reboot into your new system!
+        </para>
+      </listitem>
+    </orderedlist>
+  </section>
+  <section xml:id="sec-installation-manual">
+    <title>Manual Installation</title>
     <para>
-      The installation media contains a basic NixOS installation. When
-      it’s finished booting, it should have detected most of your
-      hardware.
+      NixOS can be installed on BIOS or UEFI systems. The procedure for
+      a UEFI installation is broadly the same as for a BIOS
+      installation. The differences are mentioned in the following
+      steps.
     </para>
     <para>
       The NixOS manual is available by running
-      <literal>nixos-help</literal>.
+      <literal>nixos-help</literal> in the command line or from the
+      application menu in the desktop environment.
+    </para>
+    <para>
+      To have access to the command line on the graphical images, open
+      Terminal (GNOME) or Konsole (Plasma) from the application menu.
     </para>
     <para>
       You are logged-in automatically as <literal>nixos</literal>. The
@@ -31,11 +217,8 @@
 $ sudo -i
 </programlisting>
     <para>
-      If you downloaded the graphical ISO image, you can run
-      <literal>systemctl start display-manager</literal> to start the
-      desktop environment. If you want to continue on the terminal, you
-      can use <literal>loadkeys</literal> to switch to your preferred
-      keyboard layout. (We even provide neo2 via
+      You can use <literal>loadkeys</literal> to switch to your
+      preferred keyboard layout. (We even provide neo2 via
       <literal>loadkeys de neo</literal>!)
     </para>
     <para>
@@ -49,9 +232,13 @@ $ sudo -i
       bootloader lists boot entries, select the serial console boot
       entry.
     </para>
-    <section xml:id="sec-installation-booting-networking">
+    <section xml:id="sec-installation-manual-networking">
       <title>Networking in the installer</title>
       <para>
+        <anchor xml:id="sec-installation-booting-networking" />
+        <!-- legacy anchor -->
+      </para>
+      <para>
         The boot process should have brought up networking (check
         <literal>ip a</literal>). Networking is necessary for the
         installer, since it will download lots of stuff (such as source
@@ -130,502 +317,527 @@ OK
         able to login.
       </para>
     </section>
-  </section>
-  <section xml:id="sec-installation-partitioning">
-    <title>Partitioning and formatting</title>
-    <para>
-      The NixOS installer doesn’t do any partitioning or formatting, so
-      you need to do that yourself.
-    </para>
-    <para>
-      The NixOS installer ships with multiple partitioning tools. The
-      examples below use <literal>parted</literal>, but also provides
-      <literal>fdisk</literal>, <literal>gdisk</literal>,
-      <literal>cfdisk</literal>, and <literal>cgdisk</literal>.
-    </para>
-    <para>
-      The recommended partition scheme differs depending if the computer
-      uses <emphasis>Legacy Boot</emphasis> or
-      <emphasis>UEFI</emphasis>.
-    </para>
-    <section xml:id="sec-installation-partitioning-UEFI">
-      <title>UEFI (GPT)</title>
+    <section xml:id="sec-installation-manual-partitioning">
+      <title>Partitioning and formatting</title>
       <para>
-        Here's an example partition scheme for UEFI, using
-        <literal>/dev/sda</literal> as the device.
+        <anchor xml:id="sec-installation-partitioning" />
+        <!-- legacy anchor -->
       </para>
-      <note>
+      <para>
+        The NixOS installer doesn’t do any partitioning or formatting,
+        so you need to do that yourself.
+      </para>
+      <para>
+        The NixOS installer ships with multiple partitioning tools. The
+        examples below use <literal>parted</literal>, but also provides
+        <literal>fdisk</literal>, <literal>gdisk</literal>,
+        <literal>cfdisk</literal>, and <literal>cgdisk</literal>.
+      </para>
+      <para>
+        The recommended partition scheme differs depending if the
+        computer uses <emphasis>Legacy Boot</emphasis> or
+        <emphasis>UEFI</emphasis>.
+      </para>
+      <section xml:id="sec-installation-manual-partitioning-UEFI">
+        <title>UEFI (GPT)</title>
         <para>
-          You can safely ignore <literal>parted</literal>'s
-          informational message about needing to update /etc/fstab.
+          <anchor xml:id="sec-installation-partitioning-UEFI" />
+          <!-- legacy anchor -->
         </para>
-      </note>
-      <orderedlist numeration="arabic">
-        <listitem>
+        <para>
+          Here's an example partition scheme for UEFI, using
+          <literal>/dev/sda</literal> as the device.
+        </para>
+        <note>
           <para>
-            Create a <emphasis>GPT</emphasis> partition table.
+            You can safely ignore <literal>parted</literal>'s
+            informational message about needing to update /etc/fstab.
           </para>
-          <programlisting>
+        </note>
+        <orderedlist numeration="arabic">
+          <listitem>
+            <para>
+              Create a <emphasis>GPT</emphasis> partition table.
+            </para>
+            <programlisting>
 # parted /dev/sda -- mklabel gpt
 </programlisting>
-        </listitem>
-        <listitem>
-          <para>
-            Add the <emphasis>root</emphasis> partition. This will fill
-            the disk except for the end part, where the swap will live,
-            and the space left in front (512MiB) which will be used by
-            the boot partition.
-          </para>
-          <programlisting>
+          </listitem>
+          <listitem>
+            <para>
+              Add the <emphasis>root</emphasis> partition. This will
+              fill the disk except for the end part, where the swap will
+              live, and the space left in front (512MiB) which will be
+              used by the boot partition.
+            </para>
+            <programlisting>
 # parted /dev/sda -- mkpart primary 512MB -8GB
 </programlisting>
-        </listitem>
-        <listitem>
-          <para>
-            Next, add a <emphasis>swap</emphasis> partition. The size
-            required will vary according to needs, here a 8GB one is
-            created.
-          </para>
-          <programlisting>
+          </listitem>
+          <listitem>
+            <para>
+              Next, add a <emphasis>swap</emphasis> partition. The size
+              required will vary according to needs, here a 8GB one is
+              created.
+            </para>
+            <programlisting>
 # parted /dev/sda -- mkpart primary linux-swap -8GB 100%
 </programlisting>
-          <note>
+            <note>
+              <para>
+                The swap partition size rules are no different than for
+                other Linux distributions.
+              </para>
+            </note>
+          </listitem>
+          <listitem>
             <para>
-              The swap partition size rules are no different than for
-              other Linux distributions.
+              Finally, the <emphasis>boot</emphasis> partition. NixOS by
+              default uses the ESP (EFI system partition) as its
+              <emphasis>/boot</emphasis> partition. It uses the
+              initially reserved 512MiB at the start of the disk.
             </para>
-          </note>
-        </listitem>
-        <listitem>
-          <para>
-            Finally, the <emphasis>boot</emphasis> partition. NixOS by
-            default uses the ESP (EFI system partition) as its
-            <emphasis>/boot</emphasis> partition. It uses the initially
-            reserved 512MiB at the start of the disk.
-          </para>
-          <programlisting>
+            <programlisting>
 # parted /dev/sda -- mkpart ESP fat32 1MB 512MB
 # parted /dev/sda -- set 3 esp on
 </programlisting>
-        </listitem>
-      </orderedlist>
-      <para>
-        Once complete, you can follow with
-        <xref linkend="sec-installation-partitioning-formatting" />.
-      </para>
-    </section>
-    <section xml:id="sec-installation-partitioning-MBR">
-      <title>Legacy Boot (MBR)</title>
-      <para>
-        Here's an example partition scheme for Legacy Boot, using
-        <literal>/dev/sda</literal> as the device.
-      </para>
-      <note>
+          </listitem>
+        </orderedlist>
         <para>
-          You can safely ignore <literal>parted</literal>'s
-          informational message about needing to update /etc/fstab.
+          Once complete, you can follow with
+          <xref linkend="sec-installation-manual-partitioning-formatting" />.
         </para>
-      </note>
-      <orderedlist numeration="arabic">
-        <listitem>
+      </section>
+      <section xml:id="sec-installation-manual-partitioning-MBR">
+        <title>Legacy Boot (MBR)</title>
+        <para>
+          <anchor xml:id="sec-installation-partitioning-MBR" />
+          <!-- legacy anchor -->
+        </para>
+        <para>
+          Here's an example partition scheme for Legacy Boot, using
+          <literal>/dev/sda</literal> as the device.
+        </para>
+        <note>
           <para>
-            Create a <emphasis>MBR</emphasis> partition table.
+            You can safely ignore <literal>parted</literal>'s
+            informational message about needing to update /etc/fstab.
           </para>
-          <programlisting>
+        </note>
+        <orderedlist numeration="arabic">
+          <listitem>
+            <para>
+              Create a <emphasis>MBR</emphasis> partition table.
+            </para>
+            <programlisting>
 # parted /dev/sda -- mklabel msdos
 </programlisting>
-        </listitem>
-        <listitem>
-          <para>
-            Add the <emphasis>root</emphasis> partition. This will fill
-            the the disk except for the end part, where the swap will
-            live.
-          </para>
-          <programlisting>
+          </listitem>
+          <listitem>
+            <para>
+              Add the <emphasis>root</emphasis> partition. This will
+              fill the the disk except for the end part, where the swap
+              will live.
+            </para>
+            <programlisting>
 # parted /dev/sda -- mkpart primary 1MB -8GB
 </programlisting>
-        </listitem>
-        <listitem>
-          <para>
-            Set the root partition’s boot flag to on. This allows the
-            disk to be booted from.
-          </para>
-          <programlisting>
+          </listitem>
+          <listitem>
+            <para>
+              Set the root partition’s boot flag to on. This allows the
+              disk to be booted from.
+            </para>
+            <programlisting>
 # parted /dev/sda -- set 1 boot on
 </programlisting>
-        </listitem>
-        <listitem>
-          <para>
-            Finally, add a <emphasis>swap</emphasis> partition. The size
-            required will vary according to needs, here a 8GiB one is
-            created.
-          </para>
-          <programlisting>
+          </listitem>
+          <listitem>
+            <para>
+              Finally, add a <emphasis>swap</emphasis> partition. The
+              size required will vary according to needs, here a 8GB one
+              is created.
+            </para>
+            <programlisting>
 # parted /dev/sda -- mkpart primary linux-swap -8GB 100%
 </programlisting>
-          <note>
+            <note>
+              <para>
+                The swap partition size rules are no different than for
+                other Linux distributions.
+              </para>
+            </note>
+          </listitem>
+        </orderedlist>
+        <para>
+          Once complete, you can follow with
+          <xref linkend="sec-installation-manual-partitioning-formatting" />.
+        </para>
+      </section>
+      <section xml:id="sec-installation-manual-partitioning-formatting">
+        <title>Formatting</title>
+        <para>
+          <anchor xml:id="sec-installation-partitioning-formatting" />
+          <!-- legacy anchor -->
+        </para>
+        <para>
+          Use the following commands:
+        </para>
+        <itemizedlist>
+          <listitem>
             <para>
-              The swap partition size rules are no different than for
-              other Linux distributions.
+              For initialising Ext4 partitions:
+              <literal>mkfs.ext4</literal>. It is recommended that you
+              assign a unique symbolic label to the file system using
+              the option <literal>-L label</literal>, since this makes
+              the file system configuration independent from device
+              changes. For example:
             </para>
-          </note>
-        </listitem>
-      </orderedlist>
-      <para>
-        Once complete, you can follow with
-        <xref linkend="sec-installation-partitioning-formatting" />.
-      </para>
+            <programlisting>
+# mkfs.ext4 -L nixos /dev/sda1
+</programlisting>
+          </listitem>
+          <listitem>
+            <para>
+              For creating swap partitions: <literal>mkswap</literal>.
+              Again it’s recommended to assign a label to the swap
+              partition: <literal>-L label</literal>. For example:
+            </para>
+            <programlisting>
+# mkswap -L swap /dev/sda2
+</programlisting>
+          </listitem>
+          <listitem>
+            <para>
+              <emphasis role="strong">UEFI systems</emphasis>
+            </para>
+            <para>
+              For creating boot partitions: <literal>mkfs.fat</literal>.
+              Again it’s recommended to assign a label to the boot
+              partition: <literal>-n label</literal>. For example:
+            </para>
+            <programlisting>
+# mkfs.fat -F 32 -n boot /dev/sda3
+</programlisting>
+          </listitem>
+          <listitem>
+            <para>
+              For creating LVM volumes, the LVM commands, e.g.,
+              <literal>pvcreate</literal>, <literal>vgcreate</literal>,
+              and <literal>lvcreate</literal>.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              For creating software RAID devices, use
+              <literal>mdadm</literal>.
+            </para>
+          </listitem>
+        </itemizedlist>
+      </section>
     </section>
-    <section xml:id="sec-installation-partitioning-formatting">
-      <title>Formatting</title>
+    <section xml:id="sec-installation-manual-installing">
+      <title>Installing</title>
       <para>
-        Use the following commands:
+        <anchor xml:id="sec-installation-installing" />
+        <!-- legacy anchor -->
       </para>
-      <itemizedlist>
+      <orderedlist numeration="arabic">
         <listitem>
           <para>
-            For initialising Ext4 partitions:
-            <literal>mkfs.ext4</literal>. It is recommended that you
-            assign a unique symbolic label to the file system using the
-            option <literal>-L label</literal>, since this makes the
-            file system configuration independent from device changes.
-            For example:
+            Mount the target file system on which NixOS should be
+            installed on <literal>/mnt</literal>, e.g.
           </para>
           <programlisting>
-# mkfs.ext4 -L nixos /dev/sda1
+# mount /dev/disk/by-label/nixos /mnt
 </programlisting>
         </listitem>
         <listitem>
           <para>
-            For creating swap partitions: <literal>mkswap</literal>.
-            Again it’s recommended to assign a label to the swap
-            partition: <literal>-L label</literal>. For example:
+            <emphasis role="strong">UEFI systems</emphasis>
+          </para>
+          <para>
+            Mount the boot file system on <literal>/mnt/boot</literal>,
+            e.g.
           </para>
           <programlisting>
-# mkswap -L swap /dev/sda2
+# mkdir -p /mnt/boot
+# mount /dev/disk/by-label/boot /mnt/boot
 </programlisting>
         </listitem>
         <listitem>
           <para>
-            <emphasis role="strong">UEFI systems</emphasis>
-          </para>
-          <para>
-            For creating boot partitions: <literal>mkfs.fat</literal>.
-            Again it’s recommended to assign a label to the boot
-            partition: <literal>-n label</literal>. For example:
+            If your machine has a limited amount of memory, you may want
+            to activate swap devices now
+            (<literal>swapon device</literal>). The installer (or
+            rather, the build actions that it may spawn) may need quite
+            a bit of RAM, depending on your configuration.
           </para>
           <programlisting>
-# mkfs.fat -F 32 -n boot /dev/sda3
+# swapon /dev/sda2
 </programlisting>
         </listitem>
         <listitem>
           <para>
-            For creating LVM volumes, the LVM commands, e.g.,
-            <literal>pvcreate</literal>, <literal>vgcreate</literal>,
-            and <literal>lvcreate</literal>.
+            You now need to create a file
+            <literal>/mnt/etc/nixos/configuration.nix</literal> that
+            specifies the intended configuration of the system. This is
+            because NixOS has a <emphasis>declarative</emphasis>
+            configuration model: you create or edit a description of the
+            desired configuration of your system, and then NixOS takes
+            care of making it happen. The syntax of the NixOS
+            configuration file is described in
+            <xref linkend="sec-configuration-syntax" />, while a list of
+            available configuration options appears in
+            <xref linkend="ch-options" />. A minimal example is shown in
+            <link linkend="ex-config">Example: NixOS
+            Configuration</link>.
           </para>
-        </listitem>
-        <listitem>
           <para>
-            For creating software RAID devices, use
-            <literal>mdadm</literal>.
+            The command <literal>nixos-generate-config</literal> can
+            generate an initial configuration file for you:
           </para>
-        </listitem>
-      </itemizedlist>
-    </section>
-  </section>
-  <section xml:id="sec-installation-installing">
-    <title>Installing</title>
-    <orderedlist numeration="arabic">
-      <listitem>
-        <para>
-          Mount the target file system on which NixOS should be
-          installed on <literal>/mnt</literal>, e.g.
-        </para>
-        <programlisting>
-# mount /dev/disk/by-label/nixos /mnt
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          <emphasis role="strong">UEFI systems</emphasis>
-        </para>
-        <para>
-          Mount the boot file system on <literal>/mnt/boot</literal>,
-          e.g.
-        </para>
-        <programlisting>
-# mkdir -p /mnt/boot
-# mount /dev/disk/by-label/boot /mnt/boot
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          If your machine has a limited amount of memory, you may want
-          to activate swap devices now
-          (<literal>swapon device</literal>). The installer (or rather,
-          the build actions that it may spawn) may need quite a bit of
-          RAM, depending on your configuration.
-        </para>
-        <programlisting>
-# swapon /dev/sda2
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          You now need to create a file
-          <literal>/mnt/etc/nixos/configuration.nix</literal> that
-          specifies the intended configuration of the system. This is
-          because NixOS has a <emphasis>declarative</emphasis>
-          configuration model: you create or edit a description of the
-          desired configuration of your system, and then NixOS takes
-          care of making it happen. The syntax of the NixOS
-          configuration file is described in
-          <xref linkend="sec-configuration-syntax" />, while a list of
-          available configuration options appears in
-          <xref linkend="ch-options" />. A minimal example is shown in
-          <link linkend="ex-config">Example: NixOS Configuration</link>.
-        </para>
-        <para>
-          The command <literal>nixos-generate-config</literal> can
-          generate an initial configuration file for you:
-        </para>
-        <programlisting>
+          <programlisting>
 # nixos-generate-config --root /mnt
 </programlisting>
-        <para>
-          You should then edit
-          <literal>/mnt/etc/nixos/configuration.nix</literal> to suit
-          your needs:
-        </para>
-        <programlisting>
+          <para>
+            You should then edit
+            <literal>/mnt/etc/nixos/configuration.nix</literal> to suit
+            your needs:
+          </para>
+          <programlisting>
 # nano /mnt/etc/nixos/configuration.nix
 </programlisting>
-        <para>
-          If you’re using the graphical ISO image, other editors may be
-          available (such as <literal>vim</literal>). If you have
-          network access, you can also install other editors – for
-          instance, you can install Emacs by running
-          <literal>nix-env -f '&lt;nixpkgs&gt;' -iA emacs</literal>.
-        </para>
-        <variablelist>
-          <varlistentry>
-            <term>
-              BIOS systems
-            </term>
-            <listitem>
-              <para>
-                You <emphasis>must</emphasis> set the option
-                <xref linkend="opt-boot.loader.grub.device" /> to
-                specify on which disk the GRUB boot loader is to be
-                installed. Without it, NixOS cannot boot.
-              </para>
-              <para>
-                If there are other operating systems running on the
-                machine before installing NixOS, the
-                <xref linkend="opt-boot.loader.grub.useOSProber" />
-                option can be set to <literal>true</literal> to
-                automatically add them to the grub menu.
-              </para>
-            </listitem>
-          </varlistentry>
-          <varlistentry>
-            <term>
-              UEFI systems
-            </term>
-            <listitem>
-              <para>
-                You must select a boot-loader, either system-boot or
-                GRUB. The recommended option is systemd-boot: set the
-                option
-                <xref linkend="opt-boot.loader.systemd-boot.enable" />
-                to <literal>true</literal>.
-                <literal>nixos-generate-config</literal> should do this
-                automatically for new configurations when booted in UEFI
-                mode.
-              </para>
-              <para>
-                You may want to look at the options starting with
-                <link linkend="opt-boot.loader.efi.canTouchEfiVariables"><literal>boot.loader.efi</literal></link>
-                and
-                <link linkend="opt-boot.loader.systemd-boot.enable"><literal>boot.loader.systemd-boot</literal></link>
-                as well.
-              </para>
-              <para>
-                If you want to use GRUB, set
-                <xref linkend="opt-boot.loader.grub.device" /> to
-                <literal>nodev</literal> and
-                <xref linkend="opt-boot.loader.grub.efiSupport" /> to
-                <literal>true</literal>.
-              </para>
-              <para>
-                With system-boot, you should not need any special
-                configuration to detect other installed systems. With
-                GRUB, set
-                <xref linkend="opt-boot.loader.grub.useOSProber" /> to
-                <literal>true</literal>, but this will only detect
-                windows partitions, not other linux distributions. If
-                you dual boot another linux distribution, use
-                system-boot instead.
-              </para>
-            </listitem>
-          </varlistentry>
-        </variablelist>
-        <para>
-          If you need to configure networking for your machine the
-          configuration options are described in
-          <xref linkend="sec-networking" />. In particular, while wifi
-          is supported on the installation image, it is not enabled by
-          default in the configuration generated by
-          <literal>nixos-generate-config</literal>.
-        </para>
-        <para>
-          Another critical option is <literal>fileSystems</literal>,
-          specifying the file systems that need to be mounted by NixOS.
-          However, you typically don’t need to set it yourself, because
-          <literal>nixos-generate-config</literal> sets it automatically
-          in
-          <literal>/mnt/etc/nixos/hardware-configuration.nix</literal>
-          from your currently mounted file systems. (The configuration
-          file <literal>hardware-configuration.nix</literal> is included
-          from <literal>configuration.nix</literal> and will be
-          overwritten by future invocations of
-          <literal>nixos-generate-config</literal>; thus, you generally
-          should not modify it.) Additionally, you may want to look at
-          <link xlink:href="https://github.com/NixOS/nixos-hardware">Hardware
-          configuration for known-hardware</link> at this point or after
-          installation.
-        </para>
-        <note>
           <para>
-            Depending on your hardware configuration or type of file
-            system, you may need to set the option
-            <literal>boot.initrd.kernelModules</literal> to include the
-            kernel modules that are necessary for mounting the root file
-            system, otherwise the installed system will not be able to
-            boot. (If this happens, boot from the installation media
-            again, mount the target file system on
-            <literal>/mnt</literal>, fix
-            <literal>/mnt/etc/nixos/configuration.nix</literal> and
-            rerun <literal>nixos-install</literal>.) In most cases,
-            <literal>nixos-generate-config</literal> will figure out the
-            required modules.
+            If you’re using the graphical ISO image, other editors may
+            be available (such as <literal>vim</literal>). If you have
+            network access, you can also install other editors – for
+            instance, you can install Emacs by running
+            <literal>nix-env -f '&lt;nixpkgs&gt;' -iA emacs</literal>.
           </para>
-        </note>
-      </listitem>
-      <listitem>
-        <para>
-          Do the installation:
-        </para>
-        <programlisting>
+          <variablelist>
+            <varlistentry>
+              <term>
+                BIOS systems
+              </term>
+              <listitem>
+                <para>
+                  You <emphasis>must</emphasis> set the option
+                  <xref linkend="opt-boot.loader.grub.device" /> to
+                  specify on which disk the GRUB boot loader is to be
+                  installed. Without it, NixOS cannot boot.
+                </para>
+                <para>
+                  If there are other operating systems running on the
+                  machine before installing NixOS, the
+                  <xref linkend="opt-boot.loader.grub.useOSProber" />
+                  option can be set to <literal>true</literal> to
+                  automatically add them to the grub menu.
+                </para>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>
+                UEFI systems
+              </term>
+              <listitem>
+                <para>
+                  You must select a boot-loader, either system-boot or
+                  GRUB. The recommended option is systemd-boot: set the
+                  option
+                  <xref linkend="opt-boot.loader.systemd-boot.enable" />
+                  to <literal>true</literal>.
+                  <literal>nixos-generate-config</literal> should do
+                  this automatically for new configurations when booted
+                  in UEFI mode.
+                </para>
+                <para>
+                  You may want to look at the options starting with
+                  <link linkend="opt-boot.loader.efi.canTouchEfiVariables"><literal>boot.loader.efi</literal></link>
+                  and
+                  <link linkend="opt-boot.loader.systemd-boot.enable"><literal>boot.loader.systemd-boot</literal></link>
+                  as well.
+                </para>
+                <para>
+                  If you want to use GRUB, set
+                  <xref linkend="opt-boot.loader.grub.device" /> to
+                  <literal>nodev</literal> and
+                  <xref linkend="opt-boot.loader.grub.efiSupport" /> to
+                  <literal>true</literal>.
+                </para>
+                <para>
+                  With system-boot, you should not need any special
+                  configuration to detect other installed systems. With
+                  GRUB, set
+                  <xref linkend="opt-boot.loader.grub.useOSProber" /> to
+                  <literal>true</literal>, but this will only detect
+                  windows partitions, not other linux distributions. If
+                  you dual boot another linux distribution, use
+                  system-boot instead.
+                </para>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+          <para>
+            If you need to configure networking for your machine the
+            configuration options are described in
+            <xref linkend="sec-networking" />. In particular, while wifi
+            is supported on the installation image, it is not enabled by
+            default in the configuration generated by
+            <literal>nixos-generate-config</literal>.
+          </para>
+          <para>
+            Another critical option is <literal>fileSystems</literal>,
+            specifying the file systems that need to be mounted by
+            NixOS. However, you typically don’t need to set it yourself,
+            because <literal>nixos-generate-config</literal> sets it
+            automatically in
+            <literal>/mnt/etc/nixos/hardware-configuration.nix</literal>
+            from your currently mounted file systems. (The configuration
+            file <literal>hardware-configuration.nix</literal> is
+            included from <literal>configuration.nix</literal> and will
+            be overwritten by future invocations of
+            <literal>nixos-generate-config</literal>; thus, you
+            generally should not modify it.) Additionally, you may want
+            to look at
+            <link xlink:href="https://github.com/NixOS/nixos-hardware">Hardware
+            configuration for known-hardware</link> at this point or
+            after installation.
+          </para>
+          <note>
+            <para>
+              Depending on your hardware configuration or type of file
+              system, you may need to set the option
+              <literal>boot.initrd.kernelModules</literal> to include
+              the kernel modules that are necessary for mounting the
+              root file system, otherwise the installed system will not
+              be able to boot. (If this happens, boot from the
+              installation media again, mount the target file system on
+              <literal>/mnt</literal>, fix
+              <literal>/mnt/etc/nixos/configuration.nix</literal> and
+              rerun <literal>nixos-install</literal>.) In most cases,
+              <literal>nixos-generate-config</literal> will figure out
+              the required modules.
+            </para>
+          </note>
+        </listitem>
+        <listitem>
+          <para>
+            Do the installation:
+          </para>
+          <programlisting>
 # nixos-install
 </programlisting>
-        <para>
-          This will install your system based on the configuration you
-          provided. If anything fails due to a configuration problem or
-          any other issue (such as a network outage while downloading
-          binaries from the NixOS binary cache), you can re-run
-          <literal>nixos-install</literal> after fixing your
-          <literal>configuration.nix</literal>.
-        </para>
-        <para>
-          As the last step, <literal>nixos-install</literal> will ask
-          you to set the password for the <literal>root</literal> user,
-          e.g.
-        </para>
-        <programlisting>
+          <para>
+            This will install your system based on the configuration you
+            provided. If anything fails due to a configuration problem
+            or any other issue (such as a network outage while
+            downloading binaries from the NixOS binary cache), you can
+            re-run <literal>nixos-install</literal> after fixing your
+            <literal>configuration.nix</literal>.
+          </para>
+          <para>
+            As the last step, <literal>nixos-install</literal> will ask
+            you to set the password for the <literal>root</literal>
+            user, e.g.
+          </para>
+          <programlisting>
 setting root password...
 New password: ***
 Retype new password: ***
 </programlisting>
-        <note>
+          <note>
+            <para>
+              For unattended installations, it is possible to use
+              <literal>nixos-install --no-root-passwd</literal> in order
+              to disable the password prompt entirely.
+            </para>
+          </note>
+        </listitem>
+        <listitem>
           <para>
-            For unattended installations, it is possible to use
-            <literal>nixos-install --no-root-passwd</literal> in order
-            to disable the password prompt entirely.
+            If everything went well:
           </para>
-        </note>
-      </listitem>
-      <listitem>
-        <para>
-          If everything went well:
-        </para>
-        <programlisting>
+          <programlisting>
 # reboot
 </programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          You should now be able to boot into the installed NixOS. The
-          GRUB boot menu shows a list of <emphasis>available
-          configurations</emphasis> (initially just one). Every time you
-          change the NixOS configuration (see
-          <link linkend="sec-changing-config">Changing
-          Configuration</link>), a new item is added to the menu. This
-          allows you to easily roll back to a previous configuration if
-          something goes wrong.
-        </para>
-        <para>
-          You should log in and change the <literal>root</literal>
-          password with <literal>passwd</literal>.
-        </para>
-        <para>
-          You’ll probably want to create some user accounts as well,
-          which can be done with <literal>useradd</literal>:
-        </para>
-        <programlisting>
+        </listitem>
+        <listitem>
+          <para>
+            You should now be able to boot into the installed NixOS. The
+            GRUB boot menu shows a list of <emphasis>available
+            configurations</emphasis> (initially just one). Every time
+            you change the NixOS configuration (see
+            <link linkend="sec-changing-config">Changing
+            Configuration</link>), a new item is added to the menu. This
+            allows you to easily roll back to a previous configuration
+            if something goes wrong.
+          </para>
+          <para>
+            You should log in and change the <literal>root</literal>
+            password with <literal>passwd</literal>.
+          </para>
+          <para>
+            You’ll probably want to create some user accounts as well,
+            which can be done with <literal>useradd</literal>:
+          </para>
+          <programlisting>
 $ useradd -c 'Eelco Dolstra' -m eelco
 $ passwd eelco
 </programlisting>
-        <para>
-          You may also want to install some software. This will be
-          covered in <xref linkend="sec-package-management" />.
-        </para>
-      </listitem>
-    </orderedlist>
-  </section>
-  <section xml:id="sec-installation-summary">
-    <title>Installation summary</title>
-    <para>
-      To summarise, <link linkend="ex-install-sequence">Example:
-      Commands for Installing NixOS on
-      <literal>/dev/sda</literal></link> shows a typical sequence of
-      commands for installing NixOS on an empty hard drive (here
-      <literal>/dev/sda</literal>). <link linkend="ex-config">Example:
-      NixOS Configuration</link> shows a corresponding configuration Nix
-      expression.
-    </para>
-    <anchor xml:id="ex-partition-scheme-MBR" />
-    <para>
-      <emphasis role="strong">Example: Example partition schemes for
-      NixOS on <literal>/dev/sda</literal> (MBR)</emphasis>
-    </para>
-    <programlisting>
+          <para>
+            You may also want to install some software. This will be
+            covered in <xref linkend="sec-package-management" />.
+          </para>
+        </listitem>
+      </orderedlist>
+    </section>
+    <section xml:id="sec-installation-manual-summary">
+      <title>Installation summary</title>
+      <para>
+        <anchor xml:id="sec-installation-summary" />
+        <!-- legacy anchor -->
+      </para>
+      <para>
+        To summarise, <link linkend="ex-install-sequence">Example:
+        Commands for Installing NixOS on
+        <literal>/dev/sda</literal></link> shows a typical sequence of
+        commands for installing NixOS on an empty hard drive (here
+        <literal>/dev/sda</literal>). <link linkend="ex-config">Example:
+        NixOS Configuration</link> shows a corresponding configuration
+        Nix expression.
+      </para>
+      <anchor xml:id="ex-partition-scheme-MBR" />
+      <para>
+        <emphasis role="strong">Example: Example partition schemes for
+        NixOS on <literal>/dev/sda</literal> (MBR)</emphasis>
+      </para>
+      <programlisting>
 # parted /dev/sda -- mklabel msdos
-# parted /dev/sda -- mkpart primary 1MiB -8GiB
-# parted /dev/sda -- mkpart primary linux-swap -8GiB 100%
+# parted /dev/sda -- mkpart primary 1MB -8GB
+# parted /dev/sda -- mkpart primary linux-swap -8GB 100%
 </programlisting>
-    <anchor xml:id="ex-partition-scheme-UEFI" />
-    <para>
-      <emphasis role="strong">Example: Example partition schemes for
-      NixOS on <literal>/dev/sda</literal> (UEFI)</emphasis>
-    </para>
-    <programlisting>
+      <anchor xml:id="ex-partition-scheme-UEFI" />
+      <para>
+        <emphasis role="strong">Example: Example partition schemes for
+        NixOS on <literal>/dev/sda</literal> (UEFI)</emphasis>
+      </para>
+      <programlisting>
 # parted /dev/sda -- mklabel gpt
-# parted /dev/sda -- mkpart primary 512MiB -8GiB
-# parted /dev/sda -- mkpart primary linux-swap -8GiB 100%
-# parted /dev/sda -- mkpart ESP fat32 1MiB 512MiB
+# parted /dev/sda -- mkpart primary 512MB -8GB
+# parted /dev/sda -- mkpart primary linux-swap -8GB 100%
+# parted /dev/sda -- mkpart ESP fat32 1MB 512MB
 # parted /dev/sda -- set 3 esp on
 </programlisting>
-    <anchor xml:id="ex-install-sequence" />
-    <para>
-      <emphasis role="strong">Example: Commands for Installing NixOS on
-      <literal>/dev/sda</literal></emphasis>
-    </para>
-    <para>
-      With a partitioned disk.
-    </para>
-    <programlisting>
+      <anchor xml:id="ex-install-sequence" />
+      <para>
+        <emphasis role="strong">Example: Commands for Installing NixOS
+        on <literal>/dev/sda</literal></emphasis>
+      </para>
+      <para>
+        With a partitioned disk.
+      </para>
+      <programlisting>
 # mkfs.ext4 -L nixos /dev/sda1
 # mkswap -L swap /dev/sda2
 # swapon /dev/sda2
@@ -638,11 +850,11 @@ $ passwd eelco
 # nixos-install
 # reboot
 </programlisting>
-    <anchor xml:id="ex-config" />
-    <para>
-      <emphasis role="strong">Example: NixOS Configuration</emphasis>
-    </para>
-    <programlisting>
+      <anchor xml:id="ex-config" />
+      <para>
+        <emphasis role="strong">Example: NixOS Configuration</emphasis>
+      </para>
+      <programlisting>
 { config, pkgs, ... }: {
   imports = [
     # Include the results of the hardware scan.
@@ -661,6 +873,7 @@ $ passwd eelco
   services.sshd.enable = true;
 }
 </programlisting>
+    </section>
   </section>
   <section xml:id="sec-installation-additional-notes">
     <title>Additional installation notes</title>
diff --git a/nixos/doc/manual/from_md/installation/obtaining.chapter.xml b/nixos/doc/manual/from_md/installation/obtaining.chapter.xml
index a922feda25366..d187adfc0c535 100644
--- a/nixos/doc/manual/from_md/installation/obtaining.chapter.xml
+++ b/nixos/doc/manual/from_md/installation/obtaining.chapter.xml
@@ -2,16 +2,15 @@
   <title>Obtaining NixOS</title>
   <para>
     NixOS ISO images can be downloaded from the
-    <link xlink:href="https://nixos.org/nixos/download.html">NixOS
-    download page</link>. There are a number of installation options. If
-    you happen to have an optical drive and a spare CD, burning the
-    image to CD and booting from that is probably the easiest option.
-    Most people will need to prepare a USB stick to boot from.
-    <xref linkend="sec-booting-from-usb" /> describes the preferred
-    method to prepare a USB stick. A number of alternative methods are
-    presented in the
-    <link xlink:href="https://nixos.wiki/wiki/NixOS_Installation_Guide#Making_the_installation_media">NixOS
-    Wiki</link>.
+    <link xlink:href="https://nixos.org/download.html#nixos-iso">NixOS
+    download page</link>. Follow the instructions in
+    <xref linkend="sec-booting-from-usb" /> to create a bootable USB
+    flash drive.
+  </para>
+  <para>
+    If you have a very old system that can’t boot from USB, you can burn
+    the image to an empty CD. NixOS might not work very well on such
+    systems.
   </para>
   <para>
     As an alternative to installing NixOS yourself, you can get a
@@ -23,16 +22,16 @@
         Using virtual appliances in Open Virtualization Format (OVF)
         that can be imported into VirtualBox. These are available from
         the
-        <link xlink:href="https://nixos.org/nixos/download.html">NixOS
+        <link xlink:href="https://nixos.org/download.html#nixos-virtualbox">NixOS
         download page</link>.
       </para>
     </listitem>
     <listitem>
       <para>
-        Using AMIs for Amazon’s EC2. To find one for your region and
-        instance type, please refer to the
-        <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/ec2-amis.nix">list
-        of most recent AMIs</link>.
+        Using AMIs for Amazon’s EC2. To find one for your region, please
+        refer to the
+        <link xlink:href="https://nixos.org/download.html#nixos-amazon">download
+        page</link>.
       </para>
     </listitem>
     <listitem>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
index 4843119805665..b7790c99a91e5 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
@@ -2106,7 +2106,7 @@ Superuser created successfully.
           <literal>ghc810</literal>. Those attributes point to the same
           compilers and packagesets but have the advantage that e.g.
           <literal>ghc92</literal> stays stable when we update from
-          <literal>ghc924</literal> to <literal>ghc925</literal>.
+          <literal>ghc925</literal> to <literal>ghc926</literal>.
         </para>
       </listitem>
     </itemizedlist>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
index 02201861234b9..97e993e83ff04 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
@@ -1501,18 +1501,18 @@
       </listitem>
       <listitem>
         <para>
-          MultiMC has been replaced with the fork PolyMC due to upstream
-          developers being hostile to 3rd party package maintainers.
-          PolyMC removes all MultiMC branding and is aimed at providing
-          proper 3rd party packages like the one contained in Nixpkgs.
-          This change affects the data folder where game instances and
-          other save and configuration files are stored. Users with
-          existing installations should rename
+          MultiMC has been replaced with the fork PrismLauncher due to
+          upstream developers being hostile to 3rd party package
+          maintainers. PrismLauncher removes all MultiMC branding and is
+          aimed at providing proper 3rd party packages like the one
+          contained in Nixpkgs. This change affects the data folder
+          where game instances and other save and configuration files
+          are stored. Users with existing installations should rename
           <literal>~/.local/share/multimc</literal> to
-          <literal>~/.local/share/polymc</literal>. The main config
-          file’s path has also moved from
+          <literal>~/.local/share/PrismLauncher</literal>. The main
+          config file’s path has also moved from
           <literal>~/.local/share/multimc/multimc.cfg</literal> to
-          <literal>~/.local/share/polymc/polymc.cfg</literal>.
+          <literal>~/.local/share/PrismLauncher/prismlauncher.cfg</literal>.
         </para>
       </listitem>
       <listitem>
@@ -2526,10 +2526,9 @@ sudo cp /var/lib/redis/dump.rdb /var/lib/redis-peertube/dump.rdb
       <listitem>
         <para>
           The logrotate module also has been updated to freeform syntax:
-          <link linkend="opt-services.logrotate.paths">services.logrotate.paths</link>
-          and
-          <link linkend="opt-services.logrotate.extraConfig">services.logrotate.extraConfig</link>
-          will work, but issue deprecation warnings and
+          <literal>services.logrotate.paths</literal> and
+          <literal>services.logrotate.extraConfig</literal> will work,
+          but issue deprecation warnings and
           <link linkend="opt-services.logrotate.settings">services.logrotate.settings</link>
           should now be used instead.
         </para>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
index da8bf17c0ccf1..1b6e9a0f7f356 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
@@ -132,6 +132,22 @@
       </listitem>
       <listitem>
         <para>
+          PHP is now built <literal>NTS</literal> (Non-Thread Safe)
+          style by default, for Apache and <literal>mod_php</literal>
+          usage we still enable <literal>ZTS</literal> (Zend Thread
+          Safe). This has been a common practice for a long time in
+          other distributions.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>protonup</literal> has been aliased to and replaced
+          by <literal>protonup-ng</literal> due to upstream not
+          maintaining it.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           Perl has been updated to 5.36, and its core module
           <literal>HTTP::Tiny</literal> was patched to verify SSL/TLS
           certificates by default.
@@ -139,6 +155,15 @@
       </listitem>
       <listitem>
         <para>
+          Improved performances of
+          <literal>lib.closePropagation</literal> which was previously
+          quadratic. This is used in e.g.
+          <literal>ghcWithPackages</literal>. Please see backward
+          incompatibilities notes below.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           Cinnamon has been updated to 5.4. While at it, the cinnamon
           module now defaults to blueman as bluetooth manager and
           slick-greeter as lightdm greeter to match upstream.
@@ -182,6 +207,23 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://github.com/maxbrunet/automatic-timezoned">automatic-timezoned</link>.
+          a Linux daemon to automatically update the system timezone
+          based on location. Available as
+          <link linkend="opt-services.automatic-timezoned.enable">services.automatic-timezoned</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          [xray] (https://github.com/XTLS/Xray-core), a fully compatible
+          v2ray-core replacement. Features XTLS, which when enabled on
+          server and client, brings UDP FullCone NAT to proxy setups.
+          Available as
+          <link xlink:href="options.html#opt-services.xray.enable">services.xray</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://github.com/mozilla-services/syncstorage-rs">syncstorage-rs</link>,
           a self-hostable sync server for Firefox. Available as
           <link xlink:href="options.html#opt-services.firefox-syncserver.enable">services.firefox-syncserver</link>.
@@ -256,6 +298,13 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://github.com/prymitive/karma">karma</link>,
+          an alert dashboard for Prometheus Alertmanager. Available as
+          <link xlink:href="options.html#opt-services.karma.enable">services.karma</link>
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://languagetool.org/">languagetool</link>,
           a multilingual grammar, style, and spell checker. Available as
           <link xlink:href="options.html#opt-services.languagetool.enable">services.languagetool</link>.
@@ -277,6 +326,13 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://ntfy.sh">ntfy.sh</link>, a push
+          notification service. Available as
+          <link linkend="opt-services.ntfy-sh.enable">services.ntfy-sh</link>
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://git.sr.ht/~migadu/alps">alps</link>,
           a simple and extensible webmail. Available as
           <link linkend="opt-services.alps.enable">services.alps</link>.
@@ -284,6 +340,13 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://github.com/skeeto/endlessh">endlessh</link>,
+          an SSH tarpit. Available as
+          <link linkend="opt-services.endlessh.enable">services.endlessh</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://github.com/shizunge/endlessh-go">endlessh-go</link>,
           an SSH tarpit that exposes Prometheus metrics. Available as
           <link linkend="opt-services.endlessh-go.enable">services.endlessh-go</link>.
@@ -291,6 +354,14 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://garagehq.deuxfleurs.fr/">Garage</link>,
+          a simple object storage server for geodistributed deployments,
+          alternative to MinIO. Available as
+          <link linkend="opt-services.garage.enable">services.garage</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://netbird.io">netbird</link>, a zero
           configuration VPN. Available as
           <link xlink:href="options.html#opt-services.netbird.enable">services.netbird</link>.
@@ -336,6 +407,13 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://troglobit.com/projects/merecat/">merecat</link>,
+          a small and easy HTTP server based on thttpd. Available as
+          <link linkend="opt-services.merecat.enable">services.merecat</link>
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://github.com/L11R/go-autoconfig">go-autoconfig</link>,
           IMAP/SMTP autodiscover server. Available as
           <link linkend="opt-services.go-autoconfig.enable">services.go-autoconfig</link>.
@@ -396,6 +474,21 @@
           <link xlink:href="options.html#opt-services.listmonk.enable">services.listmonk</link>.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://uptime.kuma.pet/">Uptime
+          Kuma</link>, a fancy self-hosted monitoring tool. Available as
+          <link linkend="opt-services.uptime-kuma.enable">services.uptime-kuma</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://mepo.milesalan.com">Mepo</link>, a
+          fast, simple, hackable OSM map viewer for mobile and desktop
+          Linux. Available as
+          <link linkend="opt-programs.mepo.enable">programs.mepo.enable</link>.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-22.11-incompatibilities">
@@ -448,6 +541,15 @@
       </listitem>
       <listitem>
         <para>
+          Deprecated settings <literal>logrotate.paths</literal> and
+          <literal>logrotate.extraConfig</literal> have been removed.
+          Please convert any uses to
+          <link linkend="opt-services.logrotate.settings">services.logrotate.settings</link>
+          instead.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>isPowerPC</literal> predicate, found on
           <literal>platform</literal> attrsets
           (<literal>hostPlatform</literal>,
@@ -475,6 +577,16 @@
       </listitem>
       <listitem>
         <para>
+          <literal>openssh</literal> was updated to version 9.1,
+          disabling the generation of DSA keys when using
+          <literal>ssh-keygen -A</literal> as they are insecure. Also,
+          <literal>SetEnv</literal> directives in
+          <literal>ssh_config</literal> and
+          <literal>sshd_config</literal> are now first-match-wins
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>bsp-layout</literal> no longer uses the command
           <literal>cycle</literal> to switch to other window layouts, as
           it got replaced by the commands <literal>previous</literal>
@@ -531,6 +643,17 @@
       </listitem>
       <listitem>
         <para>
+          The ipfs package and module were renamed to kubo. The kubo
+          module now uses an RFC42-style <literal>settings</literal>
+          option instead of <literal>extraConfig</literal> and the
+          <literal>gatewayAddress</literal>,
+          <literal>apiAddress</literal> and
+          <literal>swarmAddress</literal> options were renamed. Using
+          the old names will print a warning but still work.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>pkgs.cosign</literal> does not provide the
           <literal>cosigned</literal> binary anymore. The
           <literal>sget</literal> binary has been moved into its own
@@ -554,6 +677,15 @@
       </listitem>
       <listitem>
         <para>
+          ppd files in <literal>pkgs.cups-drv-rastertosag-gdi</literal>
+          are now gzipped. If you refer to such a ppd file with its path
+          (e.g. via
+          <link xlink:href="options.html#opt-hardware.printers.ensurePrinters">hardware.printers.ensurePrinters</link>)
+          you will need to append <literal>.gz</literal> to the path.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           xow package removed along with the
           <literal>hardware.xow</literal> module, due to the project
           being deprecated in favor of <literal>xone</literal>, which is
@@ -581,6 +713,12 @@
       </listitem>
       <listitem>
         <para>
+          <literal>lib.closePropagation</literal> now needs that all
+          gathered sets have an <literal>outPath</literal> attribute.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           lemmy module option
           <literal>services.lemmy.settings.database.createLocally</literal>
           moved to
@@ -595,6 +733,14 @@
       </listitem>
       <listitem>
         <para>
+          The <literal>nix.checkConfig</literal> option now fully
+          disables the config check. The new
+          <literal>nix.checkAllErrors</literal> option behaves like
+          <literal>nix.checkConfig</literal> previously did.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>generateOptparseApplicativeCompletions</literal> and
           <literal>generateOptparseApplicativeCompletion</literal> from
           <literal>haskell.lib.compose</literal> (and
@@ -626,6 +772,13 @@
       </listitem>
       <listitem>
         <para>
+          The <literal>trace</literal> binary from
+          <literal>perf-linux</literal> package has been removed, due to
+          being a duplicate of the <literal>perf</literal> binary.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>aws</literal> package has been removed due to
           being abandoned by the upstream. It is recommended to use
           <literal>awscli</literal> or <literal>awscli2</literal>
@@ -634,6 +787,16 @@
       </listitem>
       <listitem>
         <para>
+          The
+          <link xlink:href="https://ce-programming.github.io/CEmu">CEmu
+          TI-84 Plus CE emulator</link> package has been renamed to
+          <literal>cemu-ti</literal>. The
+          <link xlink:href="https://cemu.info">Cemu Wii U
+          emulator</link> is now packaged as <literal>cemu</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>systemd-networkd</literal> v250 deprecated, renamed,
           and moved some sections and settings which leads to the
           following breaking module changes:
@@ -675,6 +838,28 @@
       </listitem>
       <listitem>
         <para>
+          <literal>arangodb</literal> versions 3.3, 3.4, and 3.5 have
+          been removed because they are at EOL upstream. The default is
+          now 3.10.0. Support for aarch64-linux has been removed since
+          the target cannot be built reproducibly. By default
+          <literal>arangodb</literal> is now built for the
+          <literal>haswell</literal> architecture. If you wish to build
+          for a different architecture, you may override the
+          <literal>targetArchitecture</literal> argument with a value
+          from
+          <link xlink:href="https://github.com/arangodb/arangodb/blob/207ec6937e41a46e10aea34953879341f0606841/cmake/OptimizeForArchitecture.cmake#L594">this
+          list supported upstream</link>. Some architecture specific
+          optimizations are also conditionally enabled. You may alter
+          this behavior by overriding the
+          <literal>asmOptimizations</literal> parameter. You may also
+          add additional architecture support by adding more
+          <literal>-DHAS_XYZ</literal> flags to
+          <literal>cmakeFlags</literal> via
+          <literal>overrideAttrs</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>meta.mainProgram</literal> attribute of packages
           in <literal>wineWowPackages</literal> now defaults to
           <literal>&quot;wine64&quot;</literal>.
@@ -695,6 +880,12 @@
       </listitem>
       <listitem>
         <para>
+          Linux 4.9 has been removed because it will reach its end of
+          life within the lifespan of 22.11.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           (Neo)Vim can not be configured with
           <literal>configure.pathogen</literal> anymore to reduce
           maintainance burden. Use <literal>configure.packages</literal>
@@ -709,24 +900,99 @@
       </listitem>
       <listitem>
         <para>
+          The <literal>adguardhome</literal> module no longer uses
+          <literal>host</literal> and <literal>port</literal> options,
+          use <literal>settings.bind_host</literal> and
+          <literal>settings.bind_port</literal> instead.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The default <literal>kops</literal> version is now 1.25.1 and
           support for 1.22 and older has been dropped.
         </para>
       </listitem>
       <listitem>
         <para>
+          The <literal>zrepl</literal> package has been updated from
+          0.5.0 to 0.6.0. See the
+          <link xlink:href="https://zrepl.github.io/changelog.html">changelog</link>
+          for details.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>k3s</literal> no longer supports docker as runtime
           due to upstream dropping support.
         </para>
       </listitem>
       <listitem>
         <para>
+          <literal>cassandra_2_1</literal> and
+          <literal>cassandra_2_2</literal> have been removed. Please
+          update to <literal>cassandra_3_11</literal> or
+          <literal>cassandra_3_0</literal>. See the
+          <link xlink:href="https://github.com/apache/cassandra/blob/cassandra-3.11.14/NEWS.txt">changelog</link>
+          for more information about the upgrade process.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>mysql57</literal> has been removed. Please update to
+          <literal>mysql80</literal> or <literal>mariadb</literal>. See
+          the
+          <link xlink:href="https://mariadb.com/kb/en/upgrading-from-mysql-to-mariadb/">upgrade
+          guide</link> for more information.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Consequently, <literal>cqrlog</literal> and
+          <literal>amorok</literal> now use <literal>mariadb</literal>
+          instead of <literal>mysql57</literal> for their embedded
+          databases. Running <literal>mysql_upgrade</literal> may be
+          neccesary.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>k3s</literal> supports <literal>clusterInit</literal>
           option, and it is enabled by default, for servers.
         </para>
       </listitem>
       <listitem>
         <para>
+          <literal>percona-server56</literal> has been removed. Please
+          migrate to <literal>mysql</literal> or
+          <literal>mariadb</literal> if possible.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>obs-studio</literal> hase been updated to version 28.
+          If you have packaged custom plugins, check if they are
+          compatible. <literal>obs-websocket</literal> has been
+          integrated into <literal>obs-studio</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>signald</literal> has been bumped to
+          <literal>0.23.0</literal>. For the upgrade, a migration
+          process is necessary. It can be done by running a command like
+          this before starting <literal>signald.service</literal>:
+        </para>
+        <programlisting>
+signald -d /var/lib/signald/db \
+  --database sqlite:/var/lib/signald/db \
+  --migrate-data
+</programlisting>
+        <para>
+          For further information, please read the upstream changelogs.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>stylua</literal> no longer accepts
           <literal>lua52Support</literal> and
           <literal>luauSupport</literal> overrides, use
@@ -736,6 +1002,12 @@
       </listitem>
       <listitem>
         <para>
+          <literal>ocamlPackages.ocaml_extlib</literal> has been renamed
+          to <literal>ocamlPackages.extlib</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>pkgs.fetchNextcloudApp</literal> has been rewritten
           to circumvent impurities in e.g. tarballs from GitHub and to
           make it easier to apply patches. This means that your hashes
@@ -744,6 +1016,34 @@
           longer accepted.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          The Syncthing service now only allows absolute paths—starting
+          with <literal>/</literal> or <literal>~/</literal>—for
+          <literal>services.syncthing.folders.&lt;name&gt;.path</literal>.
+          In a future release other paths will be allowed again and
+          interpreted relative to
+          <literal>services.syncthing.dataDir</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>services.github-runner</literal> and
+          <literal>services.github-runners.&lt;name&gt;</literal> gained
+          the option <literal>serviceOverrides</literal> which allows
+          overriding the systemd <literal>serviceConfig</literal>. If
+          you have been overriding the systemd service configuration
+          (i.e., by defining
+          <literal>systemd.services.github-runner.serviceConfig</literal>),
+          you have to use the <literal>serviceOverrides</literal> option
+          now. Example:
+        </para>
+        <programlisting>
+services.github-runner.serviceOverrides.SupplementaryGroups = [
+  &quot;docker&quot;
+];
+</programlisting>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-22.11-notable-changes">
@@ -751,6 +1051,17 @@
     <itemizedlist>
       <listitem>
         <para>
+          <literal>firefox</literal>, <literal>thunderbird</literal> and
+          <literal>librewolf</literal> come with enabled Wayland support
+          by default. The <literal>firefox-wayland</literal>,
+          <literal>firefox-esr-wayland</literal>,
+          <literal>thunderbird-wayland</literal> and
+          <literal>librewolf-wayland</literal> attributes are obsolete
+          and have been aliased to their generic attribute.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>xplr</literal> package has been updated from
           0.18.0 to 0.19.0, which brings some breaking changes. See the
           <link xlink:href="https://github.com/sayanarijit/xplr/releases/tag/v0.19.0">upstream
@@ -759,6 +1070,13 @@
       </listitem>
       <listitem>
         <para>
+          Configuring multiple GitHub runners is now possible through
+          <literal>services.github-runners.&lt;name&gt;</literal>. The
+          option <literal>services.github-runner</literal> remains.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>github-runner</literal> gained support for ephemeral
           runners and registrations using a personal access token (PAT)
           instead of a registration token. See
@@ -777,6 +1095,25 @@
       </listitem>
       <listitem>
         <para>
+          ZFS module will not allow hibernation by default, this is a
+          safety measure to prevent data loss cases like the ones
+          described at
+          <link xlink:href="https://github.com/openzfs/zfs/issues/260">OpenZFS/260</link>
+          and
+          <link xlink:href="https://github.com/openzfs/zfs/issues/12842">OpenZFS/12842</link>.
+          Use the <literal>boot.zfs.allowHibernation</literal> option to
+          configure this behaviour.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          <literal>mastodon</literal> now automatically removes remote
+          media attachments older than 30 days. This is configurable
+          through <literal>services.mastodon.mediaAutoRemove</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The Redis module now disables RDB persistence when
           <literal>services.redis.servers.&lt;name&gt;.save = []</literal>
           instead of using the Redis default.
@@ -797,12 +1134,84 @@
       </listitem>
       <listitem>
         <para>
+          The option <literal>overrideStrategy</literal> was added to
+          the different systemd unit options
+          (<literal>systemd.services.&lt;name&gt;</literal>,
+          <literal>systemd.sockets.&lt;name&gt;</literal>, …) to allow
+          enforcing the creation of a dropin file, rather than the main
+          unit file, by setting it to <literal>asDropin</literal>. This
+          is useful in cases where the existence of the main unit file
+          is not known to Nix at evaluation time, for example when the
+          main unit file is provided by adding a package to
+          <literal>systemd.packages</literal>. See the fix proposed in
+          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/135557#issuecomment-1295392470">NixOS’s
+          systemd abstraction doesn’t work with systemd template
+          units</link> for an example.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>polymc</literal> package has been removed due to
+          a rogue maintainer. It has been replaced by
+          <literal>prismlauncher</literal>, a fork by the rest of the
+          maintainers. For more details, see
+          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/196624">the
+          pull request that made this change</link> and
+          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/196460">this
+          issue detailing the vulnerability</link>. Users with existing
+          installations should rename
+          <literal>~/.local/share/polymc</literal> to
+          <literal>~/.local/share/PrismLauncher</literal>. The main
+          config file’s path has also moved from
+          <literal>~/.local/share/polymc/polymc.cfg</literal> to
+          <literal>~/.local/share/PrismLauncher/prismlauncher.cfg</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>bloat</literal> package has been updated from
+          unstable-2022-03-31 to unstable-2022-10-25, which brings a
+          breaking change. See
+          <link xlink:href="https://git.freesoftwareextremist.com/bloat/commit/?id=887ed241d64ba5db3fd3d87194fb5595e5ad7d73">this
+          upstream commit message</link> for details.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>services.matrix-synapse</literal> systemd unit
           has been hardened.
         </para>
       </listitem>
       <listitem>
         <para>
+          The <literal>services.grafana</literal> options were converted
+          to a
+          <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">RFC
+          0042</link> configuration.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>services.grafana.provision.datasources</literal>
+          and <literal>services.grafana.provision.dashboards</literal>
+          options were converted to a
+          <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">RFC
+          0042</link> configuration. They also now support specifying
+          the provisioning YAML file with <literal>path</literal>
+          option.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>services.grafana.provision.alerting</literal>
+          option was added. It includes suboptions for every
+          alerting-related objects (with the exception of
+          <literal>notifiers</literal>), which means it’s now possible
+          to configure modern Grafana alerting declaratively.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           Matrix Synapse now requires entries in the
           <literal>state_group_edges</literal> table to be unique, in
           order to prevent accidentally introducing duplicate
@@ -865,6 +1274,13 @@
       </listitem>
       <listitem>
         <para>
+          The redis module now persists each instance’s configuration
+          file in the state directory, in order to support some more
+          advanced use cases like sentinel.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The udisks2 service, available at
           <literal>services.udisks2.enable</literal>, is now disabled by
           default. It will automatically be enabled through services and
@@ -877,11 +1293,66 @@
       </listitem>
       <listitem>
         <para>
+          Nextcloud has been updated to version
+          <emphasis role="strong">25</emphasis>. Additionally the
+          following things have changed for Nextcloud in NixOS:
+        </para>
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para>
+              For Nextcloud <emphasis role="strong">&gt;=24</emphasis>,
+              the default PHP version is 8.1.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              Nextcloud <emphasis role="strong">23</emphasis> has been
+              removed since it will reach its
+              <link xlink:href="https://github.com/nextcloud/server/wiki/Maintenance-and-Release-Schedule/d76576a12a626d53305d480a6065b57cab705d3d">end
+              of life in December 2022</link>.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              For <literal>system.stateVersion</literal> being
+              <emphasis role="strong">&gt;=22.11</emphasis>, Nextcloud
+              25 will be installed by default. For older versions,
+              Nextcloud 24 will be installed.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              Please ensure that you only upgrade on major release at a
+              time! Nextcloud doesn’t support upgrades across multiple
+              versions, i.e. an upgrade from
+              <emphasis role="strong">23</emphasis> to
+              <emphasis role="strong">25</emphasis> is only possible
+              when upgrading to <emphasis role="strong">24</emphasis>
+              first.
+            </para>
+          </listitem>
+        </itemizedlist>
+      </listitem>
+      <listitem>
+        <para>
           Add udev rules for the Teensy family of microcontrollers.
         </para>
       </listitem>
       <listitem>
         <para>
+          The Qt QML disk cache is now disabled by default. This fixes a
+          long-standing issue where updating Qt/KDE apps would sometimes
+          cause them to crash or behave strangely without explanation.
+          Those concerned about the small (~10%) performance hit to
+          application startup can re-enable the cache (and expose
+          themselves to gremlins) by setting the envrionment variable
+          <literal>QML_FORCE_DISK_CACHE</literal> to
+          <literal>1</literal> using e.g. the
+          <literal>environment.sessionVariables</literal> NixOS option.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           systemd-oomd is enabled by default. Depending on which systemd
           units have <literal>ManagedOOMSwap=kill</literal> or
           <literal>ManagedOOMMemoryPressure=kill</literal>, systemd-oomd
@@ -897,6 +1368,13 @@
       </listitem>
       <listitem>
         <para>
+          The <literal>tt-rss</literal> service performs two database
+          migrations when you first use its web UI after upgrade.
+          Consider backing up its database before updating.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>pass-secret-service</literal> package now
           includes systemd units from upstream, so adding it to the
           NixOS <literal>services.dbus.packages</literal> option will
@@ -919,6 +1397,29 @@
       </listitem>
       <listitem>
         <para>
+          The default package for
+          <literal>services.mullvad-vpn.package</literal> was changed to
+          <literal>pkgs.mullvad</literal>, allowing cross-platform usage
+          of Mullvad. <literal>pkgs.mullvad</literal> only contains the
+          Mullvad CLI tool, so users who rely on the Mullvad GUI will
+          want to change it back to <literal>pkgs.mullvad-vpn</literal>,
+          or add <literal>pkgs.mullvad-vpn</literal> to their
+          environment.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          PowerDNS has been updated from <literal>4.6.x</literal> to
+          <literal>4.7.x</literal>. Please be sure to review the
+          <link xlink:href="https://doc.powerdns.com/authoritative/upgrading.html#to-4-7-0-or-master">Upgrade
+          Notes</link> provided by upstream before upgrading. Worth
+          specifically noting is that the new Catalog Zones feature
+          comes with a mandatory schema change for the gsql database
+          backends, which has to be manually applied.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           There is a new module for the <literal>thunar</literal>
           program (the Xfce file manager), which depends on the
           <literal>xfconf</literal> dbus service, and also has a dbus
@@ -938,6 +1439,16 @@
       </listitem>
       <listitem>
         <para>
+          The Mastodon package got upgraded from the major version 3 to
+          4. See the
+          <link xlink:href="https://github.com/mastodon/mastodon/releases/tag/v4.0.0">v4.0.0
+          release notes</link> for a list of changes. On standard
+          setups, no manual migration steps are required. Nevertheless,
+          a database backup is recommended.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>nomad</literal> package now defaults to 1.3,
           which no longer has a downgrade path to releases 1.2 or older.
         </para>
@@ -954,6 +1465,42 @@
           the npm install step prunes dev dependencies.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          <literal>boot.kernel.sysctl</literal> is defined as a
+          freeformType and adds a custom merge option for
+          <quote>net.core.rmem_max</quote> (taking the highest value
+          defined to avoid conflicts between 2 services trying to set
+          that value).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The <literal>mame</literal> package does not ship with its
+          tools anymore in the default output. They were moved to a
+          separate <literal>tools</literal> output instead. For
+          convenience, <literal>mame-tools</literal> package was added
+          for those who want to use it.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          A NixOS module for Firefox has been added which allows
+          preferences and
+          <link xlink:href="https://github.com/mozilla/policy-templates/blob/master/README.md">policies</link>
+          to be set. This also allows extensions to be installed via the
+          <literal>ExtensionSettings</literal> policy. The new options
+          are under <literal>programs.firefox</literal>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          The option
+          <literal>services.picom.experimentalBackends</literal> was
+          removed since it is now the default and the option will cause
+          <literal>picom</literal> to quit instead.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
 </section>
diff --git a/nixos/doc/manual/installation/installing-usb.section.md b/nixos/doc/manual/installation/installing-usb.section.md
index d893e22e6381b..da32935a7a108 100644
--- a/nixos/doc/manual/installation/installing-usb.section.md
+++ b/nixos/doc/manual/installation/installing-usb.section.md
@@ -1,31 +1,72 @@
-# Booting from a USB Drive {#sec-booting-from-usb}
+# Booting from a USB flash drive {#sec-booting-from-usb}
 
-For systems without CD drive, the NixOS live CD can be booted from a USB
-stick. You can use the `dd` utility to write the image:
-`dd if=path-to-image of=/dev/sdX`. Be careful about specifying the correct
-drive; you can use the `lsblk` command to get a list of block devices.
+The image has to be written verbatim to the USB flash drive for it to be
+bootable on UEFI and BIOS systems. Here are the recommended tools to do that.
 
-::: {.note}
-::: {.title}
-On macOS
-:::
+## Creating bootable USB flash drive with a graphical tool {#sec-booting-from-usb-graphical}
+
+Etcher is a popular and user-friendly tool. It works on Linux, Windows and macOS.
+
+Download it from [balena.io](https://www.balena.io/etcher/), start the program,
+select the downloaded NixOS ISO, then select the USB flash drive and flash it.
 
-```ShellSession
-$ diskutil list
-[..]
-/dev/diskN (external, physical):
-   #:                       TYPE NAME                    SIZE       IDENTIFIER
-[..]
-$ diskutil unmountDisk diskN
-Unmount of all volumes on diskN was successful
-$ sudo dd if=nix.iso of=/dev/rdiskN bs=1M
-```
-
-Using the \'raw\' `rdiskN` device instead of `diskN` completes in
-minutes instead of hours. After `dd` completes, a GUI dialog \"The disk
-you inserted was not readable by this computer\" will pop up, which can
-be ignored.
+::: {.warning}
+Etcher reports errors and usage statistics by default, which can be disabled in
+the settings.
 :::
 
-The `dd` utility will write the image verbatim to the drive, making it
-the recommended option for both UEFI and non-UEFI installations.
+An alternative is [USBImager](https://bztsrc.gitlab.io/usbimager),
+which is very simple and does not connect to the internet. Download the version
+with write-only (wo) interface for your system. Start the program,
+select the image, select the USB flash drive and click "Write".
+
+## Creating bootable USB flash drive from a Terminal on Linux {#sec-booting-from-usb-linux}
+
+1. Plug in the USB flash drive.
+2. Find the corresponding device with `lsblk`. You can distinguish them by
+   their size.
+3. Make sure all partitions on the device are properly unmounted. Replace `sdX`
+   with your device (e.g. `sdb`).
+
+  ```ShellSession
+  sudo umount /dev/sdX*
+  ```
+
+4. Then use the `dd` utility to write the image to the USB flash drive.
+
+  ```ShellSession
+  sudo dd if=<path-to-image> of=/dev/sdX bs=4M conv=fsync
+  ```
+
+## Creating bootable USB flash drive from a Terminal on macOS {#sec-booting-from-usb-macos}
+
+1. Plug in the USB flash drive.
+2. Find the corresponding device with `diskutil list`. You can distinguish them
+   by their size.
+3. Make sure all partitions on the device are properly unmounted. Replace `diskX`
+   with your device (e.g. `disk1`).
+
+  ```ShellSession
+  diskutil unmountDisk diskX
+  ```
+
+4. Then use the `dd` utility to write the image to the USB flash drive.
+
+  ```ShellSession
+  sudo dd if=<path-to-image> of=/dev/rdiskX bs=4m
+  ```
+
+  After `dd` completes, a GUI dialog \"The disk
+  you inserted was not readable by this computer\" will pop up, which can
+  be ignored.
+
+  ::: {.note}
+  Using the \'raw\' `rdiskX` device instead of `diskX` with dd completes in
+  minutes instead of hours.
+  :::
+
+5. Eject the disk when it is finished.
+
+  ```ShellSession
+  diskutil eject /dev/diskX
+  ```
diff --git a/nixos/doc/manual/installation/installing.chapter.md b/nixos/doc/manual/installation/installing.chapter.md
index b1e58b14b7838..2c86cb923a5c1 100644
--- a/nixos/doc/manual/installation/installing.chapter.md
+++ b/nixos/doc/manual/installation/installing.chapter.md
@@ -1,30 +1,143 @@
 # Installing NixOS {#sec-installation}
 
-## Booting the system {#sec-installation-booting}
+## Booting from the install medium {#sec-installation-booting}
+
+To begin the installation, you have to boot your computer from the install drive.
+
+1.   Plug in the install drive. Then turn on or restart your computer.
+
+2.   Open the boot menu by pressing the appropriate key, which is usually shown
+     on the display on early boot.
+     Select the USB flash drive (the option usually contains the word "USB").
+     If you choose the incorrect drive, your computer will likely continue to
+     boot as normal. In that case restart your computer and pick a
+     different drive.
+
+     ::: {.note}
+     The key to open the boot menu is different across computer brands and even
+     models. It can be <kbd>F12</kbd>, but also <kbd>F1</kbd>,
+     <kbd>F9</kbd>, <kbd>F10</kbd>, <kbd>Enter</kbd>, <kbd>Del</kbd>,
+     <kbd>Esc</kbd> or another function key. If you are unsure and don't see
+     it on the early boot screen, you can search online for your computers
+     brand, model followed by "boot from usb".
+     The computer might not even have that feature, so you have to go into the
+     BIOS/UEFI settings to change the boot order. Again, search online for
+     details about your specific computer model.
+
+     For Apple computers with Intel processors press and hold the <kbd>⌥</kbd>
+     (Option or Alt) key until you see the boot menu. On Apple silicon press
+     and hold the power button.
+     :::
+
+     ::: {.note}
+     If your computer supports both BIOS and UEFI boot, choose the UEFI option.
+     :::
+
+     ::: {.note}
+     If you use a CD for the installation, the computer will probably boot from
+     it automatically. If not, choose the option containing the word "CD" from
+     the boot menu.
+     :::
+
+3.   Shortly after selecting the appropriate boot drive, you should be
+     presented with a menu with different installer options. Leave the default
+     and wait (or press <kbd>Enter</kbd> to speed up).
+
+4.   The graphical images will start their corresponding desktop environment
+     and the graphical installer, which can take some time. The minimal images
+     will boot to a command line. You have to follow the instructions in
+     [](#sec-installation-manual) there.
+
+## Graphical Installation {#sec-installation-graphical}
+
+The graphical installer is recommended for desktop users and will guide you
+through the installation.
+
+1.   In the "Welcome" screen, you can select the language of the Installer and
+     the installed system.
+
+     ::: {.tip}
+     Leaving the language as "American English" will make it easier to search for
+     error messages in a search engine or to report an issue.
+     :::
+
+2.   Next you should choose your location to have the timezone set correctly.
+     You can actually click on the map!
+
+     ::: {.note}
+     The installer will use an online service to guess your location based on
+     your public IP address.
+     :::
+
+3.   Then you can select the keyboard layout. The default keyboard model should
+     work well with most desktop keyboards. If you have a special keyboard or
+     notebook, your model might be in the list. Select the language you are most
+     comfortable typing in.
+
+4.   On the "Users" screen, you have to type in your display name, login name
+     and password. You can also enable an option to automatically login to the
+     desktop.
+
+5.   Then you have the option to choose a desktop environment. If you want to
+     create a custom setup with a window manager, you can select "No desktop".
+
+     ::: {.tip}
+     If you don't have a favorite desktop and don't know which one to choose,
+     you can stick to either GNOME or Plasma. They have a quite different
+     design, so you should choose whichever you like better.
+     They are both popular choices and well tested on NixOS.
+     :::
+
+6.   You have the option to allow unfree software in the next screen.
+
+7.   The easiest option in the "Partitioning" screen is "Erase disk", which will
+     delete all data from the selected disk and install the system on it.
+     Also select "Swap (with Hibernation)" in the dropdown below it.
+     You have the option to encrypt the whole disk with LUKS.
+
+     ::: {.note}
+     At the top left you see if the Installer was booted with BIOS or UEFI. If
+     you know your system supports UEFI and it shows "BIOS", reboot with the
+     correct option.
+     :::
+
+     ::: {.warning}
+     Make sure you have selected the correct disk at the top and that no
+     valuable data is still on the disk! It will be deleted when
+     formatting the disk.
+     :::
+
+8.   Check the choices you made in the "Summary" and click "Install".
+
+     ::: {.note}
+     The installation takes about 15 minutes. The time varies based on the
+     selected desktop environment, internet connection speed and disk write speed.
+     :::
+
+9.  When the install is complete, remove the USB flash drive and
+    reboot into your new system!
+
+## Manual Installation {#sec-installation-manual}
 
 NixOS can be installed on BIOS or UEFI systems. The procedure for a UEFI
-installation is by and large the same as a BIOS installation. The
-differences are mentioned in the steps that follow.
+installation is broadly the same as for a BIOS installation. The differences
+are mentioned in the following steps.
 
-The installation media can be burned to a CD, or now more commonly,
-"burned" to a USB drive (see [](#sec-booting-from-usb)).
+The NixOS manual is available by running `nixos-help` in the command line
+or from the application menu in the desktop environment.
 
-The installation media contains a basic NixOS installation. When it's
-finished booting, it should have detected most of your hardware.
-
-The NixOS manual is available by running `nixos-help`.
+To have access to the command line on the graphical images, open
+Terminal (GNOME) or Konsole (Plasma) from the application menu.
 
 You are logged-in automatically as `nixos`. The `nixos` user account has
 an empty password so you can use `sudo` without a password:
+
 ```ShellSession
 $ sudo -i
 ```
 
-If you downloaded the graphical ISO image, you can run `systemctl
-start display-manager` to start the desktop environment. If you want
-to continue on the terminal, you can use `loadkeys` to switch to your
-preferred keyboard layout. (We even provide neo2 via `loadkeys de
-neo`!)
+You can use `loadkeys` to switch to your preferred keyboard layout.
+(We even provide neo2 via `loadkeys de neo`!)
 
 If the text is too small to be legible, try `setfont ter-v32n` to
 increase the font size.
@@ -33,7 +146,8 @@ To install over a serial port connect with `115200n8` (e.g.
 `picocom -b 115200 /dev/ttyUSB0`). When the bootloader lists boot
 entries, select the serial console boot entry.
 
-### Networking in the installer {#sec-installation-booting-networking}
+### Networking in the installer {#sec-installation-manual-networking}
+[]{#sec-installation-booting-networking} <!-- legacy anchor -->
 
 The boot process should have brought up networking (check `ip
 a`). Networking is necessary for the installer, since it will
@@ -100,7 +214,8 @@ placed by mounting the image on a different machine). Alternatively you
 must set a password for either `root` or `nixos` with `passwd` to be
 able to login.
 
-## Partitioning and formatting {#sec-installation-partitioning}
+### Partitioning and formatting {#sec-installation-manual-partitioning}
+[]{#sec-installation-partitioning} <!-- legacy anchor -->
 
 The NixOS installer doesn't do any partitioning or formatting, so you
 need to do that yourself.
@@ -112,7 +227,8 @@ below use `parted`, but also provides `fdisk`, `gdisk`, `cfdisk`, and
 The recommended partition scheme differs depending if the computer uses
 *Legacy Boot* or *UEFI*.
 
-### UEFI (GPT) {#sec-installation-partitioning-UEFI}
+#### UEFI (GPT) {#sec-installation-manual-partitioning-UEFI}
+[]{#sec-installation-partitioning-UEFI} <!-- legacy anchor -->
 
 Here\'s an example partition scheme for UEFI, using `/dev/sda` as the
 device.
@@ -158,9 +274,10 @@ update /etc/fstab.
     ```
 
 Once complete, you can follow with
-[](#sec-installation-partitioning-formatting).
+[](#sec-installation-manual-partitioning-formatting).
 
-### Legacy Boot (MBR) {#sec-installation-partitioning-MBR}
+#### Legacy Boot (MBR) {#sec-installation-manual-partitioning-MBR}
+[]{#sec-installation-partitioning-MBR} <!-- legacy anchor -->
 
 Here\'s an example partition scheme for Legacy Boot, using `/dev/sda` as
 the device.
@@ -190,7 +307,7 @@ update /etc/fstab.
     ```
 
 4.  Finally, add a *swap* partition. The size required will vary
-    according to needs, here a 8GiB one is created.
+    according to needs, here a 8GB one is created.
 
     ```ShellSession
     # parted /dev/sda -- mkpart primary linux-swap -8GB 100%
@@ -202,9 +319,10 @@ update /etc/fstab.
     :::
 
 Once complete, you can follow with
-[](#sec-installation-partitioning-formatting).
+[](#sec-installation-manual-partitioning-formatting).
 
-### Formatting {#sec-installation-partitioning-formatting}
+#### Formatting {#sec-installation-manual-partitioning-formatting}
+[]{#sec-installation-partitioning-formatting} <!-- legacy anchor -->
 
 Use the following commands:
 
@@ -239,7 +357,8 @@ Use the following commands:
 
 -   For creating software RAID devices, use `mdadm`.
 
-## Installing {#sec-installation-installing}
+### Installing {#sec-installation-manual-installing}
+[]{#sec-installation-installing} <!-- legacy anchor -->
 
 1.  Mount the target file system on which NixOS should be installed on
     `/mnt`, e.g.
@@ -410,7 +529,8 @@ Use the following commands:
     You may also want to install some software. This will be covered in
     [](#sec-package-management).
 
-## Installation summary {#sec-installation-summary}
+### Installation summary {#sec-installation-manual-summary}
+[]{#sec-installation-summary} <!-- legacy anchor -->
 
 To summarise, [Example: Commands for Installing NixOS on `/dev/sda`](#ex-install-sequence)
 shows a typical sequence of commands for installing NixOS on an empty hard
@@ -423,8 +543,8 @@ corresponding configuration Nix expression.
 :::
 ```ShellSession
 # parted /dev/sda -- mklabel msdos
-# parted /dev/sda -- mkpart primary 1MiB -8GiB
-# parted /dev/sda -- mkpart primary linux-swap -8GiB 100%
+# parted /dev/sda -- mkpart primary 1MB -8GB
+# parted /dev/sda -- mkpart primary linux-swap -8GB 100%
 ```
 :::
 
@@ -434,9 +554,9 @@ corresponding configuration Nix expression.
 :::
 ```ShellSession
 # parted /dev/sda -- mklabel gpt
-# parted /dev/sda -- mkpart primary 512MiB -8GiB
-# parted /dev/sda -- mkpart primary linux-swap -8GiB 100%
-# parted /dev/sda -- mkpart ESP fat32 1MiB 512MiB
+# parted /dev/sda -- mkpart primary 512MB -8GB
+# parted /dev/sda -- mkpart primary linux-swap -8GB 100%
+# parted /dev/sda -- mkpart ESP fat32 1MB 512MB
 # parted /dev/sda -- set 3 esp on
 ```
 :::
diff --git a/nixos/doc/manual/installation/obtaining.chapter.md b/nixos/doc/manual/installation/obtaining.chapter.md
index 832ec6146a9d9..a72194ecf9853 100644
--- a/nixos/doc/manual/installation/obtaining.chapter.md
+++ b/nixos/doc/manual/installation/obtaining.chapter.md
@@ -1,24 +1,21 @@
 # Obtaining NixOS {#sec-obtaining}
 
 NixOS ISO images can be downloaded from the [NixOS download
-page](https://nixos.org/nixos/download.html). There are a number of
-installation options. If you happen to have an optical drive and a spare
-CD, burning the image to CD and booting from that is probably the
-easiest option. Most people will need to prepare a USB stick to boot
-from. [](#sec-booting-from-usb) describes the preferred method to
-prepare a USB stick. A number of alternative methods are presented in
-the [NixOS Wiki](https://nixos.wiki/wiki/NixOS_Installation_Guide#Making_the_installation_media).
+page](https://nixos.org/download.html#nixos-iso). Follow the instructions in
+[](#sec-booting-from-usb) to create a bootable USB flash drive.
+
+If you have a very old system that can't boot from USB, you can burn the image
+to an empty CD. NixOS might not work very well on such systems.
 
 As an alternative to installing NixOS yourself, you can get a running
 NixOS system through several other means:
 
 -   Using virtual appliances in Open Virtualization Format (OVF) that
     can be imported into VirtualBox. These are available from the [NixOS
-    download page](https://nixos.org/nixos/download.html).
+    download page](https://nixos.org/download.html#nixos-virtualbox).
 
--   Using AMIs for Amazon's EC2. To find one for your region and
-    instance type, please refer to the [list of most recent
-    AMIs](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/ec2-amis.nix).
+-   Using AMIs for Amazon's EC2. To find one for your region, please refer
+    to the [download page](https://nixos.org/download.html#nixos-amazon).
 
 -   Using NixOps, the NixOS-based cloud deployment tool, which allows
     you to provision VirtualBox and EC2 NixOS instances from declarative
diff --git a/nixos/doc/manual/md-to-db.sh b/nixos/doc/manual/md-to-db.sh
index 2091f9b31cd2f..beb0ff9f70828 100755
--- a/nixos/doc/manual/md-to-db.sh
+++ b/nixos/doc/manual/md-to-db.sh
@@ -19,6 +19,7 @@ pandoc_flags=(
   "--lua-filter=$DIR/../../../doc/build-aux/pandoc-filters/myst-reader/roles.lua"
   "--lua-filter=$DIR/../../../doc/build-aux/pandoc-filters/link-unix-man-references.lua"
   "--lua-filter=$DIR/../../../doc/build-aux/pandoc-filters/docbook-writer/rst-roles.lua"
+  "--lua-filter=$DIR/../../../doc/build-aux/pandoc-filters/docbook-writer/html-elements.lua"
   "--lua-filter=$DIR/../../../doc/build-aux/pandoc-filters/docbook-writer/labelless-link-is-xref.lua"
   -f "commonmark${pandoc_commonmark_enabled_extensions}+smart"
   -t docbook
diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md
index bc3bd639b1ba2..5cb3731071f32 100644
--- a/nixos/doc/manual/release-notes/rl-2111.section.md
+++ b/nixos/doc/manual/release-notes/rl-2111.section.md
@@ -576,4 +576,4 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - More jdk and jre versions are now exposed via `java-packages.compiler`.
 
-- The sets `haskell.packages` and `haskell.compiler` now contain for every ghc version an attribute with the minor version dropped. E.g. for `ghc8107` there also now exists `ghc810`. Those attributes point to the same compilers and packagesets but have the advantage that e.g. `ghc92` stays stable when we update from `ghc924` to `ghc925`.
+- The sets `haskell.packages` and `haskell.compiler` now contain for every ghc version an attribute with the minor version dropped. E.g. for `ghc8107` there also now exists `ghc810`. Those attributes point to the same compilers and packagesets but have the advantage that e.g. `ghc92` stays stable when we update from `ghc925` to `ghc926`.
diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md
index 2d2140d92d590..217aa6056cad7 100644
--- a/nixos/doc/manual/release-notes/rl-2205.section.md
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -581,7 +581,15 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - The `miller` package has been upgraded from 5.10.3 to [6.2.0](https://github.com/johnkerl/miller/releases/tag/v6.2.0). See [What's new in Miller 6](https://miller.readthedocs.io/en/latest/new-in-miller-6).
 
-- MultiMC has been replaced with the fork PolyMC due to upstream developers being hostile to 3rd party package maintainers. PolyMC removes all MultiMC branding and is aimed at providing proper 3rd party packages like the one contained in Nixpkgs. This change affects the data folder where game instances and other save and configuration files are stored. Users with existing installations should rename `~/.local/share/multimc` to `~/.local/share/polymc`. The main config file's path has also moved from `~/.local/share/multimc/multimc.cfg` to `~/.local/share/polymc/polymc.cfg`.
+- MultiMC has been replaced with the fork PrismLauncher due to upstream
+  developers being hostile to 3rd party package maintainers. PrismLauncher
+  removes all MultiMC branding and is aimed at providing proper 3rd party
+  packages like the one contained in Nixpkgs. This change affects the data
+  folder where game instances and other save and configuration files are stored.
+  Users with existing installations should rename `~/.local/share/multimc` to
+  `~/.local/share/PrismLauncher`. The main config file's path has also moved
+  from `~/.local/share/multimc/multimc.cfg` to
+  `~/.local/share/PrismLauncher/prismlauncher.cfg`.
 
 - `systemd-nspawn@.service` settings have been reverted to the default systemd behaviour. User namespaces are now activated by default. If you want to keep running nspawn containers without user namespaces you need to set `systemd.nspawn.<name>.execConfig.PrivateUsers = false`
 
@@ -898,8 +906,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [services.logrotate.enable](#opt-services.logrotate.enable) now defaults to true if any rotate path has
   been defined, and some paths have been added by default.
-- The logrotate module also has been updated to freeform syntax: [services.logrotate.paths](#opt-services.logrotate.paths)
-  and [services.logrotate.extraConfig](#opt-services.logrotate.extraConfig) will work, but issue deprecation
+- The logrotate module also has been updated to freeform syntax: `services.logrotate.paths`
+  and `services.logrotate.extraConfig` will work, but issue deprecation
   warnings and [services.logrotate.settings](#opt-services.logrotate.settings) should now be used instead.
 
 - `security.pam.ussh` has been added, which allows authorizing PAM sessions based on SSH _certificates_ held within an SSH agent, using [pam-ussh](https://github.com/uber/pam-ussh).
diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md
index 68b46a7e45abf..dab7d3723e306 100644
--- a/nixos/doc/manual/release-notes/rl-2211.section.md
+++ b/nixos/doc/manual/release-notes/rl-2211.section.md
@@ -53,8 +53,16 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - PHP now defaults to PHP 8.1, updated from 8.0.
 
+- PHP is now built `NTS` (Non-Thread Safe) style by default, for Apache and
+  `mod_php` usage we still enable `ZTS` (Zend Thread Safe). This has been a
+  common practice for a long time in other distributions.
+
+- `protonup` has been aliased to and replaced by `protonup-ng` due to upstream not maintaining it.
+
 - Perl has been updated to 5.36, and its core module `HTTP::Tiny` was patched to verify SSL/TLS certificates by default.
 
+- Improved performances of `lib.closePropagation` which was previously quadratic. This is used in e.g. `ghcWithPackages`. Please see backward incompatibilities notes below.
+
 - Cinnamon has been updated to 5.4. While at it, the cinnamon module now defaults to
   blueman as bluetooth manager and slick-greeter as lightdm greeter to match upstream.
 
@@ -69,6 +77,11 @@ In addition to numerous new and upgraded packages, this release has the followin
 ## New Services {#sec-release-22.11-new-services}
 
 - [appvm](https://github.com/jollheef/appvm), Nix based app VMs. Available as [virtualisation.appvm](options.html#opt-virtualisation.appvm.enable).
+
+- [automatic-timezoned](https://github.com/maxbrunet/automatic-timezoned). a Linux daemon to automatically update the system timezone based on location. Available as [services.automatic-timezoned](#opt-services.automatic-timezoned.enable).
+
+- [xray] (https://github.com/XTLS/Xray-core), a fully compatible v2ray-core replacement. Features XTLS, which when enabled on server and client, brings UDP FullCone NAT to proxy setups. Available as [services.xray](options.html#opt-services.xray.enable).
+
 - [syncstorage-rs](https://github.com/mozilla-services/syncstorage-rs), a self-hostable sync server for Firefox. Available as [services.firefox-syncserver](options.html#opt-services.firefox-syncserver.enable).
 
 - [dragonflydb](https://dragonflydb.io/), a modern replacement for Redis and Memcached. Available as [services.dragonflydb](#opt-services.dragonflydb.enable).
@@ -91,6 +104,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 - [kanata](https://github.com/jtroo/kanata), a tool to improve keyboard comfort and usability with advanced customization.
   Available as [services.kanata](options.html#opt-services.kanata.enable).
 
+- [karma](https://github.com/prymitive/karma), an alert dashboard for Prometheus Alertmanager. Available as [services.karma](options.html#opt-services.karma.enable)
+
 - [languagetool](https://languagetool.org/), a multilingual grammar, style, and spell checker.
   Available as [services.languagetool](options.html#opt-services.languagetool.enable).
 
@@ -98,10 +113,16 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [Outline](https://www.getoutline.com/), a wiki and knowledge base similar to Notion. Available as [services.outline](#opt-services.outline.enable).
 
+- [ntfy.sh](https://ntfy.sh), a push notification service. Available as [services.ntfy-sh](#opt-services.ntfy-sh.enable)
+
 - [alps](https://git.sr.ht/~migadu/alps), a simple and extensible webmail. Available as [services.alps](#opt-services.alps.enable).
 
+- [endlessh](https://github.com/skeeto/endlessh), an SSH tarpit. Available as [services.endlessh](#opt-services.endlessh.enable).
+
 - [endlessh-go](https://github.com/shizunge/endlessh-go), an SSH tarpit that exposes Prometheus metrics. Available as [services.endlessh-go](#opt-services.endlessh-go.enable).
 
+- [Garage](https://garagehq.deuxfleurs.fr/), a simple object storage server for geodistributed deployments, alternative to MinIO. Available as [services.garage](#opt-services.garage.enable).
+
 - [netbird](https://netbird.io), a zero configuration VPN.
   Available as [services.netbird](options.html#opt-services.netbird.enable).
 
@@ -115,6 +136,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [expressvpn](https://www.expressvpn.com), the CLI client for ExpressVPN. Available as [services.expressvpn](#opt-services.expressvpn.enable).
 
+- [merecat](https://troglobit.com/projects/merecat/), a small and easy HTTP server based on thttpd. Available as [services.merecat](#opt-services.merecat.enable)
+
 - [go-autoconfig](https://github.com/L11R/go-autoconfig), IMAP/SMTP autodiscover server. Available as [services.go-autoconfig](#opt-services.go-autoconfig.enable).
 
 - [tmate-ssh-server](https://github.com/tmate-io/tmate-ssh-server), server side part of [tmate](https://tmate.io/). Available as [services.tmate-ssh-server](#opt-services.tmate-ssh-server.enable).
@@ -132,6 +155,10 @@ Available as [services.patroni](options.html#opt-services.patroni.enable).
 
 - [Listmonk](https://listmonk.app), a self-hosted newsletter manager. Enable using [services.listmonk](options.html#opt-services.listmonk.enable).
 
+- [Uptime Kuma](https://uptime.kuma.pet/), a fancy self-hosted monitoring tool. Available as [services.uptime-kuma](#opt-services.uptime-kuma.enable).
+
+- [Mepo](https://mepo.milesalan.com), a fast, simple, hackable OSM map viewer for mobile and desktop Linux. Available as [programs.mepo.enable](#opt-programs.mepo.enable).
+
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
 ## Backward Incompatibilities {#sec-release-22.11-incompatibilities}
@@ -154,10 +181,16 @@ Available as [services.patroni](options.html#opt-services.patroni.enable).
   This got partially copied over from the minimal profile and reduces the final system size by up to 200MB.
   If you require all locales installed set the option to ``[ "all" ]``.
 
+- Deprecated settings `logrotate.paths` and `logrotate.extraConfig` have
+  been removed. Please convert any uses to
+  [services.logrotate.settings](#opt-services.logrotate.settings) instead.
+
 - The `isPowerPC` predicate, found on `platform` attrsets (`hostPlatform`, `buildPlatform`, `targetPlatform`, etc) has been removed in order to reduce confusion.  The predicate was was defined such that it matches only the 32-bit big-endian members of the POWER/PowerPC family, despite having a name which would imply a broader set of systems.  If you were using this predicate, you can replace `foo.isPowerPC` with `(with foo; isPower && is32bit && isBigEndian)`.
 
 - The `fetchgit` fetcher now uses [cone mode](https://www.git-scm.com/docs/git-sparse-checkout/2.37.0#_internalscone_mode_handling) by default for sparse checkouts. [Non-cone mode](https://www.git-scm.com/docs/git-sparse-checkout/2.37.0#_internalsnon_cone_problems) can be enabled by passing `nonConeMode = true`, but note that non-cone mode is deprecated and this option may be removed alongside a future Git update without notice.
 
+- `openssh` was updated to version 9.1, disabling the generation of DSA keys when using `ssh-keygen -A` as they are insecure. Also, `SetEnv` directives in `ssh_config` and `sshd_config` are now first-match-wins
+
 - `bsp-layout` no longer uses the command `cycle` to switch to other window layouts, as it got replaced by the commands `previous` and `next`.
 
 - The Barco ClickShare driver/client package `pkgs.clickshare-csc1` and the option `programs.clickshare-csc1.enable` have been removed,
@@ -178,6 +211,8 @@ Available as [services.patroni](options.html#opt-services.patroni.enable).
 - PHP 7.4 is no longer supported due to upstream not supporting this
   version for the entire lifecycle of the 22.11 release.
 
+- The ipfs package and module were renamed to kubo. The kubo module now uses an RFC42-style `settings` option instead of `extraConfig` and the `gatewayAddress`, `apiAddress` and `swarmAddress` options were renamed. Using the old names will print a warning but still work.
+
 - `pkgs.cosign` does not provide the `cosigned` binary anymore. The `sget` binary has been moved into its own package.
 
 - Emacs now uses the Lucid toolkit by default instead of GTK because of stability and compatibility issues.
@@ -185,17 +220,23 @@ Available as [services.patroni](options.html#opt-services.patroni.enable).
 
 - riak package removed along with `services.riak` module, due to lack of maintainer to update the package.
 
+- ppd files in `pkgs.cups-drv-rastertosag-gdi` are now gzipped.  If you refer to such a ppd file with its path (e.g. via [hardware.printers.ensurePrinters](options.html#opt-hardware.printers.ensurePrinters)) you will need to append `.gz` to the path.
+
 - xow package removed along with the `hardware.xow` module, due to the project being deprecated in favor of `xone`,  which is available via the `hardware.xone` module.
 
 - dd-agent package removed along with the `services.dd-agent` module, due to the project being deprecated in favor of `datadog-agent`,  which is available via the `services.datadog-agent` module.
 
 - `teleport` has been upgraded to major version 10. Please see upstream [upgrade instructions](https://goteleport.com/docs/ver/10.0/management/operations/upgrading/) and [release notes](https://goteleport.com/docs/ver/10.0/changelog/#1000).
 
+- `lib.closePropagation` now needs that all gathered sets have an `outPath` attribute.
+
 - lemmy module option `services.lemmy.settings.database.createLocally`
   moved to `services.lemmy.database.createLocally`.
 
 - virtlyst package and `services.virtlyst` module removed, due to lack of maintainers.
 
+- The `nix.checkConfig` option now fully disables the config check. The new `nix.checkAllErrors` option behaves like `nix.checkConfig`  previously did.
+
 - `generateOptparseApplicativeCompletions` and `generateOptparseApplicativeCompletion` from `haskell.lib.compose`
   (and `haskell.lib`) have been deprecated in favor of `generateOptparseApplicativeCompletions` (plural!) as
   provided by the haskell package sets (so `haskellPackages.generateOptparseApplicativeCompletions` etc.).
@@ -210,54 +251,131 @@ Available as [services.patroni](options.html#opt-services.patroni.enable).
   `python3.pkgs.influxgraph` packages, have been removed due to lack of upstream
   maintenance.
 
+- The `trace` binary from `perf-linux` package has been removed, due to being a duplicate of the `perf` binary.
+
 - The `aws` package has been removed due to being abandoned by the upstream. It is recommended to use `awscli` or `awscli2` instead.
 
+- The [CEmu TI-84 Plus CE emulator](https://ce-programming.github.io/CEmu) package has been renamed to `cemu-ti`. The [Cemu Wii U emulator](https://cemu.info) is now packaged as `cemu`.
+
 - `systemd-networkd` v250 deprecated, renamed, and moved some sections and settings which leads to the following breaking module changes:
 
    * `systemd.network.networks.<name>.dhcpV6PrefixDelegationConfig` is renamed to `systemd.network.networks.<name>.dhcpPrefixDelegationConfig`.
    * `systemd.network.networks.<name>.dhcpV6Config` no longer accepts the `ForceDHCPv6PDOtherInformation=` setting. Please use the `WithoutRA=` and `UseDelegatedPrefix=` settings in your `systemd.network.networks.<name>.dhcpV6Config` and the `DHCPv6Client=` setting in your `systemd.network.networks.<name>.ipv6AcceptRAConfig` to control when the DHCPv6 client is started and how the delegated prefixes are handled by the DHCPv6 client.
    * `systemd.network.networks.<name>.networkConfig` no longer accepts the `IPv6Token=` setting. Use the `Token=` setting in your `systemd.network.networks.<name>.ipv6AcceptRAConfig` instead. The `systemd.network.networks.<name>.ipv6Prefixes.*.ipv6PrefixConfig` now also accepts the `Token=` setting.
 
+- `arangodb` versions 3.3, 3.4, and 3.5 have been removed because they are at EOL upstream. The default is now 3.10.0. Support for aarch64-linux has been removed since the target cannot be built reproducibly. By default `arangodb` is now built for the `haswell` architecture. If you wish to build for a different architecture, you may override the `targetArchitecture` argument with a value from [this list supported upstream](https://github.com/arangodb/arangodb/blob/207ec6937e41a46e10aea34953879341f0606841/cmake/OptimizeForArchitecture.cmake#L594). Some architecture specific optimizations are also conditionally enabled. You may alter this behavior by overriding the `asmOptimizations` parameter. You may also add additional architecture support by adding more `-DHAS_XYZ` flags to `cmakeFlags` via `overrideAttrs`.
+
 - The `meta.mainProgram` attribute of packages in `wineWowPackages` now defaults to `"wine64"`.
 
 - The `paperless` module now defaults `PAPERLESS_TIME_ZONE` to your configured system timezone.
 
 - The top-level `termonad-with-packages` alias for `termonad` has been removed.
 
+- Linux 4.9 has been removed because it will reach its end of life within the lifespan of 22.11.
+
 - (Neo)Vim can not be configured with `configure.pathogen` anymore to reduce maintainance burden.
   Use `configure.packages` instead.
 - Neovim can not be configured with plug anymore (still works for vim).
 
+- The `adguardhome` module no longer uses `host` and `port` options, use `settings.bind_host` and `settings.bind_port` instead.
+
 - The default `kops` version is now 1.25.1 and support for 1.22 and older has been dropped.
 
+- The `zrepl` package has been updated from 0.5.0 to 0.6.0. See the [changelog](https://zrepl.github.io/changelog.html) for details.
+
 - `k3s` no longer supports docker as runtime due to upstream dropping support.
 
+- `cassandra_2_1` and `cassandra_2_2` have been removed. Please update to `cassandra_3_11` or `cassandra_3_0`. See the [changelog](https://github.com/apache/cassandra/blob/cassandra-3.11.14/NEWS.txt) for more information about the upgrade process.
+
+- `mysql57` has been removed. Please update to `mysql80` or `mariadb`. See the [upgrade guide](https://mariadb.com/kb/en/upgrading-from-mysql-to-mariadb/) for more information.
+
+- Consequently, `cqrlog` and `amorok` now use `mariadb` instead of `mysql57` for their embedded databases. Running `mysql_upgrade` may be neccesary.
 - `k3s` supports `clusterInit` option, and it is enabled by default, for servers.
 
+- `percona-server56` has been removed. Please migrate to `mysql` or `mariadb` if possible.
+
+- `obs-studio` hase been updated to version 28. If you have packaged custom plugins, check if they are compatible. `obs-websocket` has been integrated into `obs-studio`.
+
+- `signald` has been bumped to `0.23.0`. For the upgrade, a migration process is necessary. It can be
+  done by running a command like this before starting `signald.service`:
+
+  ```
+  signald -d /var/lib/signald/db \
+    --database sqlite:/var/lib/signald/db \
+    --migrate-data
+  ```
+
+  For further information, please read the upstream changelogs.
+
 - `stylua` no longer accepts `lua52Support` and `luauSupport` overrides, use `features` instead, which defaults to `[ "lua54" "luau" ]`.
 
+- `ocamlPackages.ocaml_extlib` has been renamed to `ocamlPackages.extlib`.
+
 - `pkgs.fetchNextcloudApp` has been rewritten to circumvent impurities in e.g. tarballs from GitHub and to make it easier to
   apply patches. This means that your hashes are out-of-date and the (previously required) attributes `name` and `version`
   are no longer accepted.
 
+- The Syncthing service now only allows absolute paths---starting with `/` or
+  `~/`---for `services.syncthing.folders.<name>.path`.
+  In a future release other paths will be allowed again and interpreted
+  relative to `services.syncthing.dataDir`.
+
+- `services.github-runner` and `services.github-runners.<name>` gained the option `serviceOverrides` which allows overriding the systemd `serviceConfig`. If you have been overriding the systemd service configuration (i.e., by defining `systemd.services.github-runner.serviceConfig`), you have to use the `serviceOverrides` option now. Example:
+
+  ```
+  services.github-runner.serviceOverrides.SupplementaryGroups = [
+    "docker"
+  ];
+  ```
+
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
 ## Other Notable Changes {#sec-release-22.11-notable-changes}
 
+- `firefox`, `thunderbird` and `librewolf` come with enabled Wayland support by default. The `firefox-wayland`, `firefox-esr-wayland`, `thunderbird-wayland` and `librewolf-wayland` attributes are obsolete and have been aliased to their generic attribute.
+
 - The `xplr` package has been updated from 0.18.0 to 0.19.0, which brings some breaking changes. See the [upstream release notes](https://github.com/sayanarijit/xplr/releases/tag/v0.19.0) for more details.
 
+- Configuring multiple GitHub runners is now possible through `services.github-runners.<name>`. The option `services.github-runner` remains.
+
 - `github-runner` gained support for ephemeral runners and registrations using a personal access token (PAT) instead of a registration token. See `services.github-runner.ephemeral` and `services.github-runner.tokenFile` for details.
 
 - A new module was added for the Saleae Logic device family, providing the options `hardware.saleae-logic.enable` and `hardware.saleae-logic.package`.
 
+- ZFS module will not allow hibernation by default, this is a safety measure to prevent data loss cases like the ones described at [OpenZFS/260](https://github.com/openzfs/zfs/issues/260) and [OpenZFS/12842](https://github.com/openzfs/zfs/issues/12842). Use the `boot.zfs.allowHibernation` option to configure this behaviour.
+
+- `mastodon` now automatically removes remote media attachments older than 30 days. This is configurable through `services.mastodon.mediaAutoRemove`.
+
 - The Redis module now disables RDB persistence when `services.redis.servers.<name>.save = []` instead of using the Redis default.
 
 - Neo4j was updated from version 3 to version 4. See this [migration guide](https://neo4j.com/docs/upgrade-migration-guide/current/) on how to migrate your Neo4j instance.
 
 - The `networking.wireguard` module now can set the mtu on interfaces and tag its packets with an fwmark.
 
+- The option `overrideStrategy` was added to the different systemd unit options (`systemd.services.<name>`, `systemd.sockets.<name>`, …) to allow enforcing the creation of a dropin file, rather than the main unit file, by setting it to `asDropin`.
+  This is useful in cases where the existence of the main unit file is not known to Nix at evaluation time, for example when the main unit file is provided by adding a package to `systemd.packages`.
+  See the fix proposed in [NixOS's systemd abstraction doesn't work with systemd template units](https://github.com/NixOS/nixpkgs/issues/135557#issuecomment-1295392470) for an example.
+
+- The `polymc` package has been removed due to a rogue maintainer. It has been
+  replaced by `prismlauncher`, a fork by the rest of the maintainers. For more
+  details, see [the pull request that made this
+  change](https://github.com/NixOS/nixpkgs/pull/196624) and [this issue
+  detailing the vulnerability](https://github.com/NixOS/nixpkgs/issues/196460).
+  Users with existing installations should rename `~/.local/share/polymc` to
+  `~/.local/share/PrismLauncher`. The main config file's path has also moved
+  from `~/.local/share/polymc/polymc.cfg` to
+  `~/.local/share/PrismLauncher/prismlauncher.cfg`.
+
+- The `bloat` package has been updated from unstable-2022-03-31 to unstable-2022-10-25, which brings a breaking change. See [this upstream commit message](https://git.freesoftwareextremist.com/bloat/commit/?id=887ed241d64ba5db3fd3d87194fb5595e5ad7d73) for details.
+
 - The `services.matrix-synapse` systemd unit has been hardened.
 
+- The `services.grafana` options were converted to a [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) configuration.
+
+- The `services.grafana.provision.datasources` and `services.grafana.provision.dashboards` options were converted to a [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) configuration. They also now support specifying the provisioning YAML file with `path` option.
+
+- The `services.grafana.provision.alerting` option was added. It includes suboptions for every alerting-related objects (with the exception of `notifiers`), which means it's now possible to configure modern Grafana alerting declaratively.
+
 - Matrix Synapse now requires entries in the `state_group_edges` table to be unique, in order to prevent accidentally introducing duplicate information (for example, because a database backup was restored multiple times). If your Synapse database already has duplicate rows in this table, this could fail with an error and require manual remediation.
 
 - The `diamond` package has been update from 0.8.36 to 2.0.15. See the [upstream release notes](https://github.com/bbuchfink/diamond/releases) for more details.
@@ -273,11 +391,31 @@ Available as [services.patroni](options.html#opt-services.patroni.enable).
 
 - The `documentation.nixos.options.allowDocBook` option was added to ease the transition to CommonMark option documentation. Setting this option to `false` causes an error for every option included in the manual that uses DocBook documentation; it defaults to `true` to preserve the previous behavior and will be removed once the transition to CommonMark is complete.
 
+- The redis module now persists each instance's configuration file in the state directory, in order to support some more advanced use cases like sentinel.
+
 - The udisks2 service, available at `services.udisks2.enable`, is now disabled by default. It will automatically be enabled through services and desktop environments as needed.
   This also means that polkit will now actually be disabled by default. The default for `security.polkit.enable` was already flipped in the previous release, but udisks2 being enabled by default re-enabled it.
 
+- Nextcloud has been updated to version **25**. Additionally the following things have changed
+  for Nextcloud in NixOS:
+  - For Nextcloud **>=24**, the default PHP version is 8.1.
+  - Nextcloud **23** has been removed since it will reach its [end of life in December 2022](https://github.com/nextcloud/server/wiki/Maintenance-and-Release-Schedule/d76576a12a626d53305d480a6065b57cab705d3d).
+  - For `system.stateVersion` being **>=22.11**, Nextcloud 25 will be installed by default. For older versions,
+    Nextcloud 24 will be installed.
+  - Please ensure that you only upgrade on major release at a time! Nextcloud doesn't support
+    upgrades across multiple versions, i.e. an upgrade from **23** to **25** is only possible
+    when upgrading to **24** first.
+
 - Add udev rules for the Teensy family of microcontrollers.
 
+- The Qt QML disk cache is now disabled by default. This fixes a 
+  long-standing issue where updating Qt/KDE apps would sometimes cause 
+  them to crash or behave strangely without explanation. Those concerned 
+  about the small (~10%) performance hit to application startup can 
+  re-enable the cache (and expose themselves to gremlins) by setting the 
+  envrionment variable `QML_FORCE_DISK_CACHE` to `1` using e.g. the 
+  `environment.sessionVariables` NixOS option.
+
 - systemd-oomd is enabled by default. Depending on which systemd units have
   `ManagedOOMSwap=kill` or `ManagedOOMMemoryPressure=kill`, systemd-oomd will
   SIGKILL all the processes under the appropriate descendant cgroups when the
@@ -287,18 +425,34 @@ Available as [services.patroni](options.html#opt-services.patroni.enable).
   [systemd.oomd.enableSystemSlice](options.html#opt-systemd.oomd.enableSystemSlice),
   and [systemd.oomd.enableUserServices](options.html#opt-systemd.oomd.enableUserServices).
 
+- The `tt-rss` service performs two database migrations when you first use its web UI after upgrade. Consider backing up its database before updating.
+
 - The `pass-secret-service` package now includes systemd units from upstream, so adding it to the NixOS `services.dbus.packages` option will make it start automatically as a systemd user service when an application tries to talk to the libsecret D-Bus API.
 
 - There is a new module for AMD SEV CPU functionality, which grants access to the hardware.
 
 - The Wordpress module got support for installing language packs through `services.wordpress.sites.<site>.languages`.
 
+- The default package for `services.mullvad-vpn.package` was changed to `pkgs.mullvad`, allowing cross-platform usage of Mullvad. `pkgs.mullvad` only contains the Mullvad CLI tool, so users who rely on the Mullvad GUI will want to change it back to `pkgs.mullvad-vpn`, or add `pkgs.mullvad-vpn` to their environment.
+
+- PowerDNS has been updated from `4.6.x` to `4.7.x`. Please be sure to review the [Upgrade Notes](https://doc.powerdns.com/authoritative/upgrading.html#to-4-7-0-or-master) provided by upstream before upgrading. Worth specifically noting is that the new Catalog Zones feature comes with a mandatory schema change for the gsql database backends, which has to be manually applied.
+
 - There is a new module for the `thunar` program (the Xfce file manager), which depends on the `xfconf` dbus service, and also has a dbus service and a systemd unit. The option `services.xserver.desktopManager.xfce.thunarPlugins` has been renamed to `programs.thunar.plugins`, and in a future release it may be removed.
 
 - There is a new module for the `xfconf` program (the Xfce configuration storage system), which has a dbus service.
 
+- The Mastodon package got upgraded from the major version 3 to 4. See the [v4.0.0 release notes](https://github.com/mastodon/mastodon/releases/tag/v4.0.0) for a list of changes. On standard setups, no manual migration steps are required. Nevertheless, a database backup is recommended.
+
 - The `nomad` package now defaults to 1.3, which no longer has a downgrade path to releases 1.2 or older.
 
 - The `nodePackages` package set now defaults to the LTS release in the `nodejs` package again, instead of being pinned to `nodejs-14_x`. Several updates to node2nix have been made for compatibility with newer Node.js and npm versions and a new `postRebuild` hook has been added for packages to perform extra build steps before the npm install step prunes dev dependencies.
 
+- `boot.kernel.sysctl` is defined as a freeformType and adds a custom merge option for "net.core.rmem_max" (taking the highest value defined to avoid conflicts between 2 services trying to set that value).
+
+- The `mame` package does not ship with its tools anymore in the default output. They were moved to a separate `tools` output instead. For convenience, `mame-tools` package was added for those who want to use it.
+
+- A NixOS module for Firefox has been added which allows preferences and [policies](https://github.com/mozilla/policy-templates/blob/master/README.md) to be set. This also allows extensions to be installed via the `ExtensionSettings` policy. The new options are under `programs.firefox`.
+
+- The option `services.picom.experimentalBackends` was removed since it is now the default and the option will cause `picom` to quit instead.
+
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
diff --git a/nixos/lib/make-options-doc/default.nix b/nixos/lib/make-options-doc/default.nix
index 6a1bb868c20de..e097aa5eebd8e 100644
--- a/nixos/lib/make-options-doc/default.nix
+++ b/nixos/lib/make-options-doc/default.nix
@@ -40,6 +40,8 @@
 # `false`, and a different renderer may be used with different bugs and performance
 # characteristics but (hopefully) indistinguishable output.
 , allowDocBook ? true
+# whether lib.mdDoc is required for descriptions to be read as markdown.
+, markdownByDefault ? false
 }:
 
 let
@@ -122,10 +124,14 @@ in rec {
 
   optionsJSON = pkgs.runCommand "options.json"
     { meta.description = "List of NixOS options in JSON format";
-      buildInputs = [
+      nativeBuildInputs = [
         pkgs.brotli
         (let
-          self = (pkgs.python3Minimal.override {
+          # python3Minimal can't be overridden with packages on Darwin, due to a missing framework.
+          # Instead of modifying stdenv, we take the easy way out, since most people on Darwin will
+          # just be hacking on the Nixpkgs manual (which also uses make-options-doc).
+          python = if pkgs.stdenv.isDarwin then pkgs.python3 else pkgs.python3Minimal;
+          self = (python.override {
             inherit self;
             includeSiteCustomize = true;
            });
@@ -148,6 +154,7 @@ in rec {
       python ${./mergeJSON.py} \
         ${lib.optionalString warningsAreErrors "--warnings-are-errors"} \
         ${lib.optionalString (! allowDocBook) "--error-on-docbook"} \
+        ${lib.optionalString markdownByDefault "--markdown-by-default"} \
         $baseJSON $options \
         > $dst/options.json
 
diff --git a/nixos/lib/make-options-doc/generateCommonMark.py b/nixos/lib/make-options-doc/generateCommonMark.py
index 404e53b0df9c2..bf487bd89c3f8 100644
--- a/nixos/lib/make-options-doc/generateCommonMark.py
+++ b/nixos/lib/make-options-doc/generateCommonMark.py
@@ -3,7 +3,7 @@ import sys
 
 options = json.load(sys.stdin)
 for (name, value) in options.items():
-    print('##', name.replace('<', '\\<').replace('>', '\\>'))
+    print('##', name.replace('<', '&lt;').replace('>', '&gt;'))
     print(value['description'])
     print()
     if 'type' in value:
diff --git a/nixos/lib/make-options-doc/mergeJSON.py b/nixos/lib/make-options-doc/mergeJSON.py
index 8a8498746bf6c..750cd24fc653d 100644
--- a/nixos/lib/make-options-doc/mergeJSON.py
+++ b/nixos/lib/make-options-doc/mergeJSON.py
@@ -201,19 +201,27 @@ def convertMD(options: Dict[str, Any]) -> str:
         return option[key]['_type'] == typ
 
     for (name, option) in options.items():
-        if optionIs(option, 'description', 'mdDoc'):
-            option['description'] = convertString(name, option['description']['text'])
-        if optionIs(option, 'example', 'literalMD'):
-            docbook = convertString(name, option['example']['text'])
-            option['example'] = { '_type': 'literalDocBook', 'text': docbook }
-        if optionIs(option, 'default', 'literalMD'):
-            docbook = convertString(name, option['default']['text'])
-            option['default'] = { '_type': 'literalDocBook', 'text': docbook }
+        try:
+            if optionIs(option, 'description', 'mdDoc'):
+                option['description'] = convertString(name, option['description']['text'])
+            elif markdownByDefault:
+                option['description'] = convertString(name, option['description'])
+
+            if optionIs(option, 'example', 'literalMD'):
+                docbook = convertString(name, option['example']['text'])
+                option['example'] = { '_type': 'literalDocBook', 'text': docbook }
+            if optionIs(option, 'default', 'literalMD'):
+                docbook = convertString(name, option['default']['text'])
+                option['default'] = { '_type': 'literalDocBook', 'text': docbook }
+        except Exception as e:
+            raise Exception(f"Failed to render option {name}: {str(e)}")
+
 
     return options
 
 warningsAreErrors = False
 errorOnDocbook = False
+markdownByDefault = False
 optOffset = 0
 for arg in sys.argv[1:]:
     if arg == "--warnings-are-errors":
@@ -222,6 +230,9 @@ for arg in sys.argv[1:]:
     if arg == "--error-on-docbook":
         optOffset += 1
         errorOnDocbook = True
+    if arg == "--markdown-by-default":
+        optOffset += 1
+        markdownByDefault = True
 
 options = pivot(json.load(open(sys.argv[1 + optOffset], 'r')))
 overrides = pivot(json.load(open(sys.argv[2 + optOffset], 'r')))
diff --git a/nixos/lib/systemd-lib.nix b/nixos/lib/systemd-lib.nix
index 779dadece582b..4c52643446ed4 100644
--- a/nixos/lib/systemd-lib.nix
+++ b/nixos/lib/systemd-lib.nix
@@ -187,11 +187,14 @@ in rec {
         done
       done
 
-      # Symlink all units defined by systemd.units. If these are also
-      # provided by systemd or systemd.packages, then add them as
+      # Symlink units defined by systemd.units where override strategy
+      # shall be automatically detected. If these are also provided by
+      # systemd or systemd.packages, then add them as
       # <unit-name>.d/overrides.conf, which makes them extend the
       # upstream unit.
-      for i in ${toString (mapAttrsToList (n: v: v.unit) units)}; do
+      for i in ${toString (mapAttrsToList
+          (n: v: v.unit)
+          (lib.filterAttrs (n: v: (attrByPath [ "overrideStrategy" ] "asDropinIfExists" v) == "asDropinIfExists") units))}; do
         fn=$(basename $i/*)
         if [ -e $out/$fn ]; then
           if [ "$(readlink -f $i/$fn)" = /dev/null ]; then
@@ -210,11 +213,21 @@ in rec {
         fi
       done
 
+      # Symlink units defined by systemd.units which shall be
+      # treated as drop-in file.
+      for i in ${toString (mapAttrsToList
+          (n: v: v.unit)
+          (lib.filterAttrs (n: v: v ? overrideStrategy && v.overrideStrategy == "asDropin") units))}; do
+        fn=$(basename $i/*)
+        mkdir -p $out/$fn.d
+        ln -s $i/$fn $out/$fn.d/overrides.conf
+      done
+
       # Create service aliases from aliases option.
       ${concatStrings (mapAttrsToList (name: unit:
           concatMapStrings (name2: ''
             ln -sfn '${name}' $out/'${name2}'
-          '') unit.aliases) units)}
+          '') (unit.aliases or [])) units)}
 
       # Create .wants and .requires symlinks from the wantedBy and
       # requiredBy options.
@@ -222,13 +235,13 @@ in rec {
           concatMapStrings (name2: ''
             mkdir -p $out/'${name2}.wants'
             ln -sfn '../${name}' $out/'${name2}.wants'/
-          '') unit.wantedBy) units)}
+          '') (unit.wantedBy or [])) units)}
 
       ${concatStrings (mapAttrsToList (name: unit:
           concatMapStrings (name2: ''
             mkdir -p $out/'${name2}.requires'
             ln -sfn '../${name}' $out/'${name2}.requires'/
-          '') unit.requiredBy) units)}
+          '') (unit.requiredBy or [])) units)}
 
       ${optionalString (type == "system") ''
         # Stupid misc. symlinks.
@@ -340,7 +353,7 @@ in rec {
     '';
 
   targetToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
+    { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
       text =
         ''
           [Unit]
@@ -349,7 +362,7 @@ in rec {
     };
 
   serviceToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
+    { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
       text = commonUnitText def +
         ''
           [Service]
@@ -371,7 +384,7 @@ in rec {
     };
 
   socketToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
+    { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
       text = commonUnitText def +
         ''
           [Socket]
@@ -382,7 +395,7 @@ in rec {
     };
 
   timerToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
+    { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
       text = commonUnitText def +
         ''
           [Timer]
@@ -391,7 +404,7 @@ in rec {
     };
 
   pathToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
+    { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
       text = commonUnitText def +
         ''
           [Path]
@@ -400,7 +413,7 @@ in rec {
     };
 
   mountToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
+    { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
       text = commonUnitText def +
         ''
           [Mount]
@@ -409,7 +422,7 @@ in rec {
     };
 
   automountToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
+    { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
       text = commonUnitText def +
         ''
           [Automount]
@@ -418,7 +431,7 @@ in rec {
     };
 
   sliceToUnit = name: def:
-    { inherit (def) aliases wantedBy requiredBy enable;
+    { inherit (def) aliases wantedBy requiredBy enable overrideStrategy;
       text = commonUnitText def +
         ''
           [Slice]
diff --git a/nixos/lib/systemd-unit-options.nix b/nixos/lib/systemd-unit-options.nix
index 1c56b1b9aa049..44f26572a23ba 100644
--- a/nixos/lib/systemd-unit-options.nix
+++ b/nixos/lib/systemd-unit-options.nix
@@ -48,14 +48,29 @@ in rec {
       '';
     };
 
+    overrideStrategy = mkOption {
+      default = "asDropinIfExists";
+      type = types.enum [ "asDropinIfExists" "asDropin" ];
+      description = lib.mdDoc ''
+        Defines how unit configuration is provided for systemd:
+
+        `asDropinIfExists` creates a unit file when no unit file is provided by the package
+        otherwise a drop-in file name `overrides.conf`.
+
+        `asDropin` creates a drop-in file named `overrides.conf`.
+        Mainly needed to define instances for systemd template units (e.g. `systemd-nspawn@mycontainer.service`).
+
+        See also systemd.unit(1).
+      '';
+    };
+
     requiredBy = mkOption {
       default = [];
       type = types.listOf unitNameType;
       description = lib.mdDoc ''
-        Units that require (i.e. depend on and need to go down with)
-        this unit. The discussion under `wantedBy`
-        applies here as well: inverse `.requires`
-        symlinks are established.
+        Units that require (i.e. depend on and need to go down with) this unit.
+        As discussed in the `wantedBy` option description this also creates
+        `.requires` symlinks automatically.
       '';
     };
 
@@ -63,16 +78,17 @@ in rec {
       default = [];
       type = types.listOf unitNameType;
       description = lib.mdDoc ''
-        Units that want (i.e. depend on) this unit. The standard way
-        to make a unit start by default at boot is to set this option
-        to `[ "multi-user.target" ]`. That's despite
-        the fact that the systemd.unit(5) manpage says this option
-        goes in the `[Install]` section that controls
-        the behaviour of `systemctl enable`. Since
-        such a process is stateful and thus contrary to the design of
-        NixOS, setting this option instead causes the equivalent
-        inverse `.wants` symlink to be present,
-        establishing the same desired relationship in a stateless way.
+        Units that want (i.e. depend on) this unit. The default method for
+        starting a unit by default at boot time is to set this option to
+        '["multi-user.target"]' for system services. Likewise for user units
+        (`systemd.user.<name>.*`) set it to `["default.target"]` to make a unit
+        start by default when the user `<name>` logs on.
+
+        This option creates a `.wants` symlink in the given target that exists
+        statelessly without the need for running `systemctl enable`.
+        The in systemd.unit(5) manpage described `[Install]` section however is
+        not supported because it is a stateful process that does not fit well
+        into the NixOS design.
       '';
     };
 
diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix
index d7204a2bc1434..134d38f1b6767 100644
--- a/nixos/lib/testing-python.nix
+++ b/nixos/lib/testing-python.nix
@@ -9,9 +9,6 @@
   # Modules to add to each VM
 , extraConfigurations ? [ ]
 }:
-
-with pkgs;
-
 let
   nixos-lib = import ./default.nix { inherit (pkgs) lib; };
 in
diff --git a/nixos/lib/utils.nix b/nixos/lib/utils.nix
index f646f70323e35..9eefa80d1c8b7 100644
--- a/nixos/lib/utils.nix
+++ b/nixos/lib/utils.nix
@@ -39,11 +39,19 @@ rec {
     || hasPrefix a'.mountPoint b'.mountPoint
     || any (hasPrefix a'.mountPoint) b'.depends;
 
-  # Escape a path according to the systemd rules, e.g. /dev/xyzzy
-  # becomes dev-xyzzy.  FIXME: slow.
-  escapeSystemdPath = s:
-   replaceChars ["/" "-" " "] ["-" "\\x2d" "\\x20"]
-   (removePrefix "/" s);
+  # Escape a path according to the systemd rules. FIXME: slow
+  # The rules are described in systemd.unit(5) as follows:
+  # The escaping algorithm operates as follows: given a string, any "/" character is replaced by "-", and all other characters which are not ASCII alphanumerics, ":", "_" or "." are replaced by C-style "\x2d" escapes. In addition, "." is replaced with such a C-style escape when it would appear as the first character in the escaped string.
+  # When the input qualifies as absolute file system path, this algorithm is extended slightly: the path to the root directory "/" is encoded as single dash "-". In addition, any leading, trailing or duplicate "/" characters are removed from the string before transformation. Example: /foo//bar/baz/ becomes "foo-bar-baz".
+  escapeSystemdPath = s: let
+    replacePrefix = p: r: s: (if (hasPrefix p s) then r + (removePrefix p s) else s);
+    trim = s: removeSuffix "/" (removePrefix "/" s);
+    normalizedPath = strings.normalizePath s;
+  in
+    replaceChars ["/"] ["-"]
+    (replacePrefix "." (strings.escapeC ["."] ".")
+    (strings.escapeC (stringToCharacters " !\"#$%&'()*+,;<=>=@[\\]^`{|}~-")
+    (if normalizedPath == "/" then normalizedPath else trim normalizedPath)));
 
   # Quotes an argument for use in Exec* service lines.
   # systemd accepts "-quoted strings with escape sequences, toJSON produces
diff --git a/nixos/modules/config/console.nix b/nixos/modules/config/console.nix
index 1f8f80a554d0b..f5db5dc5dfc11 100644
--- a/nixos/modules/config/console.nix
+++ b/nixos/modules/config/console.nix
@@ -34,14 +34,16 @@ let
       "/share/unimaps"
     ];
   };
-
-  setVconsole = !config.boot.isContainer;
 in
 
 {
   ###### interface
 
   options.console  = {
+    enable = mkEnableOption (lib.mdDoc "virtual console") // {
+      default = true;
+    };
+
     font = mkOption {
       type = with types; either str path;
       default = "Lat2-Terminus16";
@@ -125,11 +127,17 @@ in
           '');
     }
 
-    (mkIf (!setVconsole) {
-      systemd.services.systemd-vconsole-setup.enable = false;
+    (mkIf (!cfg.enable) {
+      systemd.services = {
+        "serial-getty@ttyS0".enable = false;
+        "serial-getty@hvc0".enable = false;
+        "getty@tty1".enable = false;
+        "autovt@".enable = false;
+        systemd-vconsole-setup.enable = false;
+      };
     })
 
-    (mkIf setVconsole (mkMerge [
+    (mkIf cfg.enable (mkMerge [
       { environment.systemPackages = [ pkgs.kbd ];
 
         # Let systemd-vconsole-setup.service do the work of setting up the
diff --git a/nixos/modules/config/gtk/gtk-icon-cache.nix b/nixos/modules/config/gtk/gtk-icon-cache.nix
index 87d5483e36ab9..62f0cc3f090f9 100644
--- a/nixos/modules/config/gtk/gtk-icon-cache.nix
+++ b/nixos/modules/config/gtk/gtk-icon-cache.nix
@@ -52,10 +52,8 @@ with lib;
 
     environment.extraSetup = ''
       # For each icon theme directory ...
-
-      find $out/share/icons -mindepth 1 -maxdepth 1 -print0 | while read -d $'\0' themedir
+      find $out/share/icons -exec test -d {} ';' -mindepth 1 -maxdepth 1 -print0 | while read -d $'\0' themedir
       do
-
         # In order to build the cache, the theme dir should be
         # writable. When the theme dir is a symbolic link to somewhere
         # in the nix store it is not writable and it means that only
diff --git a/nixos/modules/config/power-management.nix b/nixos/modules/config/power-management.nix
index a4e8028cfbe9e..e7fd02920e0dc 100644
--- a/nixos/modules/config/power-management.nix
+++ b/nixos/modules/config/power-management.nix
@@ -94,7 +94,7 @@ in
         after = [ "suspend.target" "hibernate.target" "hybrid-sleep.target" "suspend-then-hibernate.target" ];
         script =
           ''
-            /run/current-system/systemd/bin/systemctl try-restart post-resume.target
+            /run/current-system/systemd/bin/systemctl try-restart --no-block post-resume.target
             ${cfg.resumeCommands}
             ${cfg.powerUpCommands}
           '';
diff --git a/nixos/modules/config/sysctl.nix b/nixos/modules/config/sysctl.nix
index b4b2d0452c4f2..4346c88f7688c 100644
--- a/nixos/modules/config/sysctl.nix
+++ b/nixos/modules/config/sysctl.nix
@@ -21,11 +21,24 @@ in
   options = {
 
     boot.kernel.sysctl = mkOption {
+      type = types.submodule {
+        freeformType = types.attrsOf sysctlOption;
+        options."net.core.rmem_max" = mkOption {
+          type = types.nullOr types.ints.unsigned // {
+            merge = loc: defs:
+              foldl
+                (a: b: if b.value == null then null else lib.max a b.value)
+                0
+                (filterOverrides defs);
+          };
+          default = null;
+          description = lib.mdDoc "The maximum socket receive buffer size. In case of conflicting values, the highest will be used.";
+        };
+      };
       default = {};
       example = literalExpression ''
         { "net.ipv4.tcp_syncookies" = false; "vm.swappiness" = 60; }
       '';
-      type = types.attrsOf sysctlOption;
       description = lib.mdDoc ''
         Runtime parameters of the Linux kernel, as set by
         {manpage}`sysctl(8)`.  Note that sysctl
@@ -35,6 +48,7 @@ in
         parameter may be a string, integer, boolean, or null
         (signifying the option will not appear at all).
       '';
+
     };
 
   };
diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl
index 5a21cb45d52be..4368ec24ea9e9 100644
--- a/nixos/modules/config/update-users-groups.pl
+++ b/nixos/modules/config/update-users-groups.pl
@@ -186,7 +186,7 @@ foreach my $name (keys %groupsCur) {
 # Rewrite /etc/group. FIXME: acquire lock.
 my @lines = map { join(":", $_->{name}, $_->{password}, $_->{gid}, $_->{members}) . "\n" }
     (sort { $a->{gid} <=> $b->{gid} } values(%groupsOut));
-updateFile($gidMapFile, to_json($gidMap));
+updateFile($gidMapFile, to_json($gidMap, {canonical => 1}));
 updateFile("/etc/group", \@lines);
 nscdInvalidate("group");
 
@@ -272,7 +272,7 @@ foreach my $name (keys %usersCur) {
 # Rewrite /etc/passwd. FIXME: acquire lock.
 @lines = map { join(":", $_->{name}, $_->{fakePassword}, $_->{uid}, $_->{gid}, $_->{description}, $_->{home}, $_->{shell}) . "\n" }
     (sort { $a->{uid} <=> $b->{uid} } (values %usersOut));
-updateFile($uidMapFile, to_json($uidMap));
+updateFile($uidMapFile, to_json($uidMap, {canonical => 1}));
 updateFile("/etc/passwd", \@lines);
 nscdInvalidate("passwd");
 
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
index dae2fde0b4e76..b538a0119c06d 100644
--- a/nixos/modules/config/users-groups.nix
+++ b/nixos/modules/config/users-groups.nix
@@ -697,7 +697,7 @@ in {
           value = "[a-zA-Z0-9/+.-]+";
           options = "${id}(=${value})?(,${id}=${value})*";
           scheme  = "${id}(${sep}${options})?";
-          content = "${base64}${sep}${base64}";
+          content = "${base64}${sep}${base64}(${sep}${base64})?";
           mcf = "^${sep}${scheme}${sep}${content}$";
         in
         if (allowsLogin user.hashedPassword
diff --git a/nixos/modules/hardware/brillo.nix b/nixos/modules/hardware/brillo.nix
index 92239de5aaecc..612061718fad0 100644
--- a/nixos/modules/hardware/brillo.nix
+++ b/nixos/modules/hardware/brillo.nix
@@ -8,13 +8,12 @@ in
   options = {
     hardware.brillo = {
       enable = mkEnableOption (lib.mdDoc ''
-        Enable brillo in userspace.
-        This will allow brightness control from users in the video group.
+        brillo in userspace.
+        This will allow brightness control from users in the video group
       '');
     };
   };
 
-
   config = mkIf cfg.enable {
     services.udev.packages = [ pkgs.brillo ];
     environment.systemPackages = [ pkgs.brillo ];
diff --git a/nixos/modules/hardware/ubertooth.nix b/nixos/modules/hardware/ubertooth.nix
index 4b87abe5beb97..e2db2068d900b 100644
--- a/nixos/modules/hardware/ubertooth.nix
+++ b/nixos/modules/hardware/ubertooth.nix
@@ -10,7 +10,7 @@ let
   };
 in {
   options.hardware.ubertooth = {
-    enable = mkEnableOption (lib.mdDoc "Enable the Ubertooth software and its udev rules.");
+    enable = mkEnableOption (lib.mdDoc "Ubertooth software and its udev rules");
 
     group = mkOption {
       type = types.str;
diff --git a/nixos/modules/hardware/usb-storage.nix b/nixos/modules/hardware/usb-storage.nix
new file mode 100644
index 0000000000000..9c1b7a125fd18
--- /dev/null
+++ b/nixos/modules/hardware/usb-storage.nix
@@ -0,0 +1,20 @@
+{ config, lib, pkgs, ... }:
+with lib;
+
+{
+  options.hardware.usbStorage.manageStartStop = mkOption {
+    type = types.bool;
+    default = true;
+    description = lib.mdDoc ''
+      Enable this option to gracefully spin-down external storage during shutdown.
+      If you suspect improper head parking after poweroff, install `smartmontools` and check
+      for the `Power-Off_Retract_Count` field for an increment.
+    '';
+  };
+
+  config = mkIf config.hardware.usbStorage.manageStartStop {
+    services.udev.extraRules = ''
+      ACTION=="add|change", SUBSYSTEM=="scsi_disk", DRIVERS=="usb-storage", ATTR{manage_start_stop}="1"
+    '';
+  };
+}
diff --git a/nixos/modules/hardware/video/nvidia.nix b/nixos/modules/hardware/video/nvidia.nix
index 25cab06119751..cee230ac41cb1 100644
--- a/nixos/modules/hardware/video/nvidia.nix
+++ b/nixos/modules/hardware/video/nvidia.nix
@@ -261,7 +261,7 @@ in
     in optional primeEnabled {
       name = igpuDriver;
       display = offloadCfg.enable;
-      modules = optional (igpuDriver == "amdgpu") [ pkgs.xorg.xf86videoamdgpu ];
+      modules = optionals (igpuDriver == "amdgpu") [ pkgs.xorg.xf86videoamdgpu ];
       deviceSection = ''
         BusID "${igpuBusId}"
         ${optionalString (syncCfg.enable && igpuDriver != "amdgpu") ''Option "AccelMethod" "none"''}
diff --git a/nixos/modules/hardware/wooting.nix b/nixos/modules/hardware/wooting.nix
index 2843dbfd7b2ba..90d046d49f4e3 100644
--- a/nixos/modules/hardware/wooting.nix
+++ b/nixos/modules/hardware/wooting.nix
@@ -3,7 +3,7 @@
 with lib;
 {
   options.hardware.wooting.enable =
-    mkEnableOption (lib.mdDoc "Enable support for Wooting keyboards");
+    mkEnableOption (lib.mdDoc "support for Wooting keyboards");
 
   config = mkIf config.hardware.wooting.enable {
     environment.systemPackages = [ pkgs.wootility ];
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix
index 8a6d30d1801a1..288cbc94a321b 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares.nix
@@ -16,5 +16,10 @@ in
     calamares-nixos-extensions
     # Needed for calamares QML module packagechooserq
     libsForQt5.full
+    # Get list of locales
+    glibcLocales
   ];
+
+  # Support choosing from any locale
+  i18n.supportedLocales = [ "all" ];
 }
diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index 35fa45dbc0140..e37142f05f41b 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -421,7 +421,7 @@ let
       echo "Usage size: $usage_size"
       echo "Image size: $image_size"
       truncate --size=$image_size "$out"
-      faketime "2000-01-01 00:00:00" mkfs.vfat -i 12345678 -n EFIBOOT "$out"
+      mkfs.vfat --invariant -i 12345678 -n EFIBOOT "$out"
 
       # Force a fixed order in mcopy for better determinism, and avoid file globbing
       for d in $(find EFI -type d | sort); do
diff --git a/nixos/modules/installer/sd-card/sd-image.nix b/nixos/modules/installer/sd-card/sd-image.nix
index cb2522d867890..ad9b803b1d1e4 100644
--- a/nixos/modules/installer/sd-card/sd-image.nix
+++ b/nixos/modules/installer/sd-card/sd-image.nix
@@ -224,14 +224,25 @@ in
         # Create a FAT32 /boot/firmware partition of suitable size into firmware_part.img
         eval $(partx $img -o START,SECTORS --nr 1 --pairs)
         truncate -s $((SECTORS * 512)) firmware_part.img
-        faketime "1970-01-01 00:00:00" mkfs.vfat -i ${config.sdImage.firmwarePartitionID} -n ${config.sdImage.firmwarePartitionName} firmware_part.img
+
+        mkfs.vfat --invariant -i ${config.sdImage.firmwarePartitionID} -n ${config.sdImage.firmwarePartitionName} firmware_part.img
 
         # Populate the files intended for /boot/firmware
         mkdir firmware
         ${config.sdImage.populateFirmwareCommands}
 
+        find firmware -exec touch --date=2000-01-01 {} +
         # Copy the populated /boot/firmware into the SD image
-        (cd firmware; mcopy -psvm -i ../firmware_part.img ./* ::)
+        cd firmware
+        # Force a fixed order in mcopy for better determinism, and avoid file globbing
+        for d in $(find . -type d -mindepth 1 | sort); do
+          faketime "2000-01-01 00:00:00" mmd -i ../firmware_part.img "::/$d"
+        done
+        for f in $(find . -type f | sort); do
+          mcopy -pvm -i ../firmware_part.img "$f" "::/$f"
+        done
+        cd ..
+
         # Verify the FAT partition before copying it.
         fsck.vfat -vn firmware_part.img
         dd conv=notrunc if=firmware_part.img of=$img seek=$START count=$SECTORS
diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix
index e85ad4efd0089..aa6e40f43705e 100644
--- a/nixos/modules/misc/documentation.nix
+++ b/nixos/modules/misc/documentation.nix
@@ -56,6 +56,7 @@ let
           )
           pkgSet;
       in scrubbedEval.options;
+
     baseOptionsJSON =
       let
         filter =
@@ -67,9 +68,9 @@ let
             );
       in
         pkgs.runCommand "lazy-options.json" {
-          libPath = filter "${toString pkgs.path}/lib";
-          pkgsLibPath = filter "${toString pkgs.path}/pkgs/pkgs-lib";
-          nixosPath = filter "${toString pkgs.path}/nixos";
+          libPath = filter (pkgs.path + "/lib");
+          pkgsLibPath = filter (pkgs.path + "/pkgs/pkgs-lib");
+          nixosPath = filter (pkgs.path + "/nixos");
           modules = map (p: ''"${removePrefix "${modulesPath}/" (toString p)}"'') docModules.lazy;
         } ''
           export NIX_STORE_DIR=$TMPDIR/store
@@ -99,6 +100,7 @@ let
               exit 1
             } >&2
         '';
+
     inherit (cfg.nixos.options) warningsAreErrors allowDocBook;
   };
 
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index cbc3b612059d7..17ea04cb4ecb5 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -355,6 +355,7 @@ in
       pipewire = 323;
       rstudio-server = 324;
       localtimed = 325;
+      automatic-timezoned = 326;
 
       # When adding a uid, make sure it doesn't match an existing gid. And don't use uids above 399!
 
@@ -664,6 +665,7 @@ in
       pipewire = 323;
       rstudio-server = 324;
       localtimed = 325;
+      automatic-timezoned = 326;
 
       # When adding a gid, make sure it doesn't match an existing
       # uid. Users and groups with the same name should have equal
diff --git a/nixos/modules/misc/man-db.nix b/nixos/modules/misc/man-db.nix
index 524199ac409cc..08fb91b3994c4 100644
--- a/nixos/modules/misc/man-db.nix
+++ b/nixos/modules/misc/man-db.nix
@@ -52,7 +52,7 @@ in
     environment.systemPackages = [ cfg.package ];
     environment.etc."man_db.conf".text =
       let
-        manualCache = pkgs.runCommandLocal "man-cache" { } ''
+        manualCache = pkgs.runCommand "man-cache" { } ''
           echo "MANDB_MAP ${cfg.manualPages}/share/man $out" > man.conf
           ${cfg.package}/bin/mandb -C man.conf -psc >/dev/null 2>&1
         '';
diff --git a/nixos/modules/misc/nixpkgs.nix b/nixos/modules/misc/nixpkgs.nix
index e8becb6bf8cc7..7f7417226d174 100644
--- a/nixos/modules/misc/nixpkgs.nix
+++ b/nixos/modules/misc/nixpkgs.nix
@@ -55,6 +55,11 @@ let
     check = builtins.isAttrs;
   };
 
+  # Whether `pkgs` was constructed by this module - not if nixpkgs.pkgs or
+  # _module.args.pkgs is set. However, determining whether _module.args.pkgs
+  # is defined elsewhere does not seem feasible.
+  constructedByMe = !opt.pkgs.isDefined;
+
   hasBuildPlatform = opt.buildPlatform.highestPrio < (mkOptionDefault {}).priority;
   hasHostPlatform = opt.hostPlatform.isDefined;
   hasPlatform = hasHostPlatform || hasBuildPlatform;
@@ -302,7 +307,7 @@ in
           ''
         else
           throw ''
-            Neither ${opt.hostPlatform} nor or the legacy option ${opt.system} has been set.
+            Neither ${opt.hostPlatform} nor the legacy option ${opt.system} has been set.
             You can set ${opt.hostPlatform} in hardware-configuration.nix by re-running
             a recent version of nixos-generate-config.
             The option ${opt.system} is still fully supported for NixOS 22.05 interoperability,
@@ -353,12 +358,12 @@ in
             else "nixpkgs.localSystem";
           pkgsSystem = finalPkgs.stdenv.targetPlatform.system;
         in {
-          assertion = !hasPlatform -> nixosExpectedSystem == pkgsSystem;
+          assertion = constructedByMe -> !hasPlatform -> nixosExpectedSystem == pkgsSystem;
           message = "The NixOS nixpkgs.pkgs option was set to a Nixpkgs invocation that compiles to target system ${pkgsSystem} but NixOS was configured for system ${nixosExpectedSystem} via NixOS option ${nixosOption}. The NixOS system settings must match the Nixpkgs target system.";
         }
       )
       {
-        assertion = hasPlatform -> legacyOptionsDefined == [];
+        assertion = constructedByMe -> hasPlatform -> legacyOptionsDefined == [];
         message = ''
           Your system configures nixpkgs with the platform parameter${optionalString hasBuildPlatform "s"}:
           ${hostPlatformLine
diff --git a/nixos/modules/misc/nixpkgs/test.nix b/nixos/modules/misc/nixpkgs/test.nix
index 9e8851707f8fc..a6d8877ae0700 100644
--- a/nixos/modules/misc/nixpkgs/test.nix
+++ b/nixos/modules/misc/nixpkgs/test.nix
@@ -59,5 +59,11 @@ lib.recurseIntoAttrs {
           For a future proof system configuration, we recommend to remove
           the legacy definitions.
         ''];
+    assert getErrors {
+        nixpkgs.localSystem = pkgs.stdenv.hostPlatform;
+        nixpkgs.hostPlatform = pkgs.stdenv.hostPlatform;
+        nixpkgs.pkgs = pkgs;
+      } == [];
+
     pkgs.emptyFile;
 }
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index c400fadef3a2a..a886332e90b70 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -82,6 +82,7 @@
   ./hardware/tuxedo-keyboard.nix
   ./hardware/ubertooth.nix
   ./hardware/usb-wwan.nix
+  ./hardware/usb-storage.nix
   ./hardware/onlykey/default.nix
   ./hardware/opentabletdriver.nix
   ./hardware/sata.nix
@@ -156,6 +157,7 @@
   ./programs/extra-container.nix
   ./programs/feedbackd.nix
   ./programs/file-roller.nix
+  ./programs/firefox.nix
   ./programs/firejail.nix
   ./programs/fish.nix
   ./programs/flashrom.nix
@@ -185,6 +187,8 @@
   ./programs/less.nix
   ./programs/liboping.nix
   ./programs/light.nix
+  ./programs/mdevctl.nix
+  ./programs/mepo.nix
   ./programs/mosh.nix
   ./programs/mininet.nix
   ./programs/msmtp.nix
@@ -318,6 +322,8 @@
   ./services/backup/zfs-replication.nix
   ./services/backup/znapzend.nix
   ./services/blockchain/ethereum/geth.nix
+  ./services/blockchain/ethereum/erigon.nix
+  ./services/blockchain/ethereum/lighthouse.nix
   ./services/backup/zrepl.nix
   ./services/cluster/corosync/default.nix
   ./services/cluster/hadoop/default.nix
@@ -347,6 +353,7 @@
   ./services/continuous-integration/hercules-ci-agent/default.nix
   ./services/continuous-integration/hydra/default.nix
   ./services/continuous-integration/github-runner.nix
+  ./services/continuous-integration/github-runners.nix
   ./services/continuous-integration/gitlab-runner.nix
   ./services/continuous-integration/gocd-agent/default.nix
   ./services/continuous-integration/gocd-server/default.nix
@@ -569,7 +576,6 @@
   ./services/misc/etcd.nix
   ./services/misc/etebase-server.nix
   ./services/misc/etesync-dav.nix
-  ./services/misc/ethminer.nix
   ./services/misc/exhibitor.nix
   ./services/misc/felix.nix
   ./services/misc/freeswitch.nix
@@ -612,6 +618,7 @@
   ./services/misc/nix-optimise.nix
   ./services/misc/nix-ssh-serve.nix
   ./services/misc/novacomd.nix
+  ./services/misc/ntfy-sh.nix
   ./services/misc/nzbget.nix
   ./services/misc/nzbhydra2.nix
   ./services/misc/octoprint.nix
@@ -684,6 +691,7 @@
   ./services/monitoring/heapster.nix
   ./services/monitoring/incron.nix
   ./services/monitoring/kapacitor.nix
+  ./services/monitoring/karma.nix
   ./services/monitoring/kthxbye.nix
   ./services/monitoring/loki.nix
   ./services/monitoring/longview.nix
@@ -714,6 +722,8 @@
   ./services/monitoring/unifi-poller.nix
   ./services/monitoring/ups.nix
   ./services/monitoring/uptime.nix
+  ./services/monitoring/vmagent.nix
+  ./services/monitoring/uptime-kuma.nix
   ./services/monitoring/vnstat.nix
   ./services/monitoring/zabbix-agent.nix
   ./services/monitoring/zabbix-proxy.nix
@@ -764,6 +774,7 @@
   ./services/networking/blockbook-frontend.nix
   ./services/networking/blocky.nix
   ./services/networking/charybdis.nix
+  ./services/networking/chisel-server.nix
   ./services/networking/cjdns.nix
   ./services/networking/cloudflare-dyndns.nix
   ./services/networking/cntlm.nix
@@ -974,6 +985,7 @@
   ./services/video/rtsp-simple-server.nix
   ./services/networking/uptermd.nix
   ./services/networking/v2ray.nix
+  ./services/networking/vdirsyncer.nix
   ./services/networking/vsftpd.nix
   ./services/networking/wasabibackend.nix
   ./services/networking/websockify.nix
@@ -985,6 +997,7 @@
   ./services/networking/xinetd.nix
   ./services/networking/xl2tpd.nix
   ./services/networking/x2goserver.nix
+  ./services/networking/xray.nix
   ./services/networking/xrdp.nix
   ./services/networking/yggdrasil.nix
   ./services/networking/zerobin.nix
@@ -1005,6 +1018,7 @@
   ./services/security/certmgr.nix
   ./services/security/cfssl.nix
   ./services/security/clamav.nix
+  ./services/security/endlessh.nix
   ./services/security/endlessh-go.nix
   ./services/security/fail2ban.nix
   ./services/security/fprintd.nix
@@ -1035,7 +1049,9 @@
   ./services/security/vault.nix
   ./services/security/vaultwarden/default.nix
   ./services/security/yubikey-agent.nix
+  ./services/system/automatic-timezoned.nix
   ./services/system/cachix-agent/default.nix
+  ./services/system/cachix-watch-store.nix
   ./services/system/cloud-init.nix
   ./services/system/dbus.nix
   ./services/system/earlyoom.nix
@@ -1069,6 +1085,7 @@
   ./services/web-apps/calibre-web.nix
   ./services/web-apps/code-server.nix
   ./services/web-apps/baget.nix
+  ./services/web-apps/changedetection-io.nix
   ./services/web-apps/convos.nix
   ./services/web-apps/dex.nix
   ./services/web-apps/discourse.nix
@@ -1140,6 +1157,7 @@
   ./services/web-servers/caddy/default.nix
   ./services/web-servers/darkhttpd.nix
   ./services/web-servers/fcgiwrap.nix
+  ./services/web-servers/garage.nix
   ./services/web-servers/hitch/default.nix
   ./services/web-servers/hydron.nix
   ./services/web-servers/jboss/default.nix
@@ -1147,6 +1165,7 @@
   ./services/web-servers/lighttpd/collectd.nix
   ./services/web-servers/lighttpd/default.nix
   ./services/web-servers/lighttpd/gitweb.nix
+  ./services/web-servers/merecat.nix
   ./services/web-servers/mighttpd2.nix
   ./services/web-servers/minio.nix
   ./services/web-servers/molly-brown.nix
@@ -1207,6 +1226,7 @@
   ./services/x11/xfs.nix
   ./services/x11/xserver.nix
   ./system/activation/activation-script.nix
+  ./system/activation/specialisation.nix
   ./system/activation/top-level.nix
   ./system/boot/binfmt.nix
   ./system/boot/emergency-mode.nix
@@ -1247,6 +1267,7 @@
   ./system/boot/systemd/user.nix
   ./system/boot/timesyncd.nix
   ./system/boot/tmp.nix
+  ./system/boot/uvesafb.nix
   ./system/etc/etc-activation.nix
   ./tasks/auto-upgrade.nix
   ./tasks/bcache.nix
diff --git a/nixos/modules/profiles/minimal.nix b/nixos/modules/profiles/minimal.nix
index 0e65989214a18..0125017dfee88 100644
--- a/nixos/modules/profiles/minimal.nix
+++ b/nixos/modules/profiles/minimal.nix
@@ -13,4 +13,9 @@ with lib;
   documentation.nixos.enable = mkDefault false;
 
   programs.command-not-found.enable = mkDefault false;
+
+  xdg.autostart.enable = mkDefault false;
+  xdg.icons.enable = mkDefault false;
+  xdg.mime.enable = mkDefault false;
+  xdg.sounds.enable = mkDefault false;
 }
diff --git a/nixos/modules/programs/bash/bash.nix b/nixos/modules/programs/bash/bash.nix
index 249e99ddc4721..286faeadc489d 100644
--- a/nixos/modules/programs/bash/bash.nix
+++ b/nixos/modules/programs/bash/bash.nix
@@ -12,7 +12,7 @@ let
   cfg = config.programs.bash;
 
   bashAliases = concatStringsSep "\n" (
-    mapAttrsFlatten (k: v: "alias ${k}=${escapeShellArg v}")
+    mapAttrsFlatten (k: v: "alias -- ${k}=${escapeShellArg v}")
       (filterAttrs (k: v: v != null) cfg.shellAliases)
   );
 
diff --git a/nixos/modules/programs/firefox.nix b/nixos/modules/programs/firefox.nix
new file mode 100644
index 0000000000000..76e6c1a553f3a
--- /dev/null
+++ b/nixos/modules/programs/firefox.nix
@@ -0,0 +1,91 @@
+{ pkgs, config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.firefox;
+
+  policyFormat = pkgs.formats.json { };
+
+  organisationInfo = ''
+    When this option is in use, Firefox will inform you that "your browser
+    is managed by your organisation". That message appears because NixOS
+    installs what you have declared here such that it cannot be overridden
+    through the user interface. It does not mean that someone else has been
+    given control of your browser, unless of course they also control your
+    NixOS configuration.
+  '';
+
+in {
+  options.programs.firefox = {
+    enable = mkEnableOption (mdDoc "the Firefox web browser");
+
+    package = mkOption {
+      description = mdDoc "Firefox package to use.";
+      type = types.package;
+      default = pkgs.firefox;
+      defaultText = literalExpression "pkgs.firefox";
+      relatedPackages = [
+        "firefox"
+        "firefox-beta-bin"
+        "firefox-bin"
+        "firefox-devedition-bin"
+        "firefox-esr"
+        "firefox-esr-wayland"
+        "firefox-wayland"
+      ];
+    };
+
+    policies = mkOption {
+      description = mdDoc ''
+        Group policies to install.
+
+        See [Mozilla's documentation](https://github.com/mozilla/policy-templates/blob/master/README.md")
+        for a list of available options.
+
+        This can be used to install extensions declaratively! Check out the
+        documentation of the `ExtensionSettings` policy for details.
+
+        ${organisationInfo}
+      '';
+      type = policyFormat.type;
+      default = {};
+    };
+
+    preferences = mkOption {
+      description = mdDoc ''
+        Preferences to set from `about://config`.
+
+        Some of these might be able to be configured more ergonomically
+        using policies.
+
+        ${organisationInfo}
+      '';
+      type = with types; attrsOf (oneOf [ bool int string ]);
+      default = {};
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    environment.etc."firefox/policies/policies.json".source =
+      let policiesJSON =
+        policyFormat.generate
+        "firefox-policies.json"
+        { inherit (cfg) policies; };
+      in mkIf (cfg.policies != {}) "${policiesJSON}";
+
+    # Preferences are converted into a policy
+    programs.firefox.policies =
+      mkIf (cfg.preferences != {})
+      {
+        Preferences = (mapAttrs (name: value: {
+          Value = value;
+          Status = "locked";
+        }) cfg.preferences);
+      };
+  };
+
+  meta.maintainers = with maintainers; [ danth ];
+}
diff --git a/nixos/modules/programs/fish.nix b/nixos/modules/programs/fish.nix
index 357105c3e79b3..160adc0cad6d3 100644
--- a/nixos/modules/programs/fish.nix
+++ b/nixos/modules/programs/fish.nix
@@ -35,7 +35,7 @@ let
     '';
 
   babelfishTranslate = path: name:
-    pkgs.runCommand "${name}.fish" {
+    pkgs.runCommandLocal "${name}.fish" {
       nativeBuildInputs = [ pkgs.babelfish ];
     } "${pkgs.babelfish}/bin/babelfish < ${path} > $out;";
 
diff --git a/nixos/modules/programs/kclock.nix b/nixos/modules/programs/kclock.nix
index 049e237187e30..63d6fb1e2d7fa 100644
--- a/nixos/modules/programs/kclock.nix
+++ b/nixos/modules/programs/kclock.nix
@@ -4,7 +4,7 @@ let
   cfg = config.programs.kclock;
   kclockPkg = pkgs.libsForQt5.kclock;
 in {
-  options.programs.kclock = { enable = mkEnableOption (lib.mdDoc "Enable KClock"); };
+  options.programs.kclock = { enable = mkEnableOption (lib.mdDoc "KClock"); };
 
   config = mkIf cfg.enable {
     services.dbus.packages = [ kclockPkg ];
diff --git a/nixos/modules/programs/less.nix b/nixos/modules/programs/less.nix
index 51a326393805c..a1134e774364a 100644
--- a/nixos/modules/programs/less.nix
+++ b/nixos/modules/programs/less.nix
@@ -103,7 +103,8 @@ in
         type = types.nullOr types.str;
         default = null;
         description = lib.mdDoc ''
-          When less closes a file opened in such a way, it will call another program, called the input postprocessor, which may  perform  any  desired  clean-up  action (such  as deleting the replacement file created by LESSOPEN).
+          When less closes a file opened in such a way, it will call another program, called the input postprocessor,
+          which may perform any desired clean-up action (such as deleting the replacement file created by LESSOPEN).
         '';
       };
     };
diff --git a/nixos/modules/programs/mdevctl.nix b/nixos/modules/programs/mdevctl.nix
new file mode 100644
index 0000000000000..2b72852333508
--- /dev/null
+++ b/nixos/modules/programs/mdevctl.nix
@@ -0,0 +1,18 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.programs.mdevctl;
+in {
+  options.programs.mdevctl = {
+    enable = mkEnableOption (lib.mdDoc "Mediated Device Management");
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ mdevctl ];
+
+    environment.etc."mdevctl.d/scripts.d/notifiers/.keep".text = "";
+    environment.etc."mdevctl.d/scripts.d/callouts/.keep".text = "";
+
+  };
+}
diff --git a/nixos/modules/programs/mepo.nix b/nixos/modules/programs/mepo.nix
new file mode 100644
index 0000000000000..4b1706a2a0e53
--- /dev/null
+++ b/nixos/modules/programs/mepo.nix
@@ -0,0 +1,46 @@
+{ pkgs, config, lib, ...}:
+with lib;
+let
+  cfg = config.programs.mepo;
+in
+{
+  options.programs.mepo = {
+    enable = mkEnableOption (mdDoc "Mepo");
+
+    locationBackends = {
+      gpsd = mkOption {
+        type = types.bool;
+        default = false;
+        description = mdDoc ''
+          Whether to enable location detection via gpsd.
+          This may require additional configuration of gpsd, see [here](#opt-services.gpsd.enable)
+        '';
+      };
+
+      geoclue = mkOption {
+        type = types.bool;
+        default = true;
+        description = mdDoc "Whether to enable location detection via geoclue";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [
+      mepo
+    ] ++ lib.optional cfg.locationBackends.geoclue geoclue2-with-demo-agent
+    ++ lib.optional cfg.locationBackends.gpsd gpsd;
+
+    services.geoclue2 = mkIf cfg.locationBackends.geoclue {
+      enable = true;
+      appConfig.where-am-i = {
+        isAllowed = true;
+        isSystem = false;
+      };
+    };
+
+    services.gpsd.enable = cfg.locationBackends.gpsd;
+  };
+
+  meta.maintainers = with maintainers; [ laalsaas ];
+}
diff --git a/nixos/modules/programs/tmux.nix b/nixos/modules/programs/tmux.nix
index cf7ea4cfcf76a..4fb9175fb8d21 100644
--- a/nixos/modules/programs/tmux.nix
+++ b/nixos/modules/programs/tmux.nix
@@ -178,6 +178,16 @@ in {
         description = lib.mdDoc "List of plugins to install.";
         example = lib.literalExpression "[ pkgs.tmuxPlugins.nord ]";
       };
+
+      withUtempter = mkOption {
+        description = lib.mdDoc ''
+          Whether to enable libutempter for tmux.
+          This is required so that tmux can write to /var/run/utmp (which can be queried with `who` to display currently connected user sessions).
+          Note, this will add a guid wrapper for the group utmp!
+        '';
+        default = true;
+        type = types.bool;
+      };
     };
   };
 
@@ -193,6 +203,15 @@ in {
         TMUX_TMPDIR = lib.optional cfg.secureSocket ''''${XDG_RUNTIME_DIR:-"/run/user/$(id -u)"}'';
       };
     };
+    security.wrappers = mkIf cfg.withUtempter {
+      utempter = {
+        source = "${pkgs.libutempter}/lib/utempter/utempter";
+        owner = "root";
+        group = "utmp";
+        setuid = false;
+        setgid = true;
+      };
+    };
   };
 
   imports = [
diff --git a/nixos/modules/programs/zsh/zsh.nix b/nixos/modules/programs/zsh/zsh.nix
index 0c59d20fee467..0b152e54cf95f 100644
--- a/nixos/modules/programs/zsh/zsh.nix
+++ b/nixos/modules/programs/zsh/zsh.nix
@@ -12,7 +12,7 @@ let
   opt = options.programs.zsh;
 
   zshAliases = concatStringsSep "\n" (
-    mapAttrsFlatten (k: v: "alias ${k}=${escapeShellArg v}")
+    mapAttrsFlatten (k: v: "alias -- ${k}=${escapeShellArg v}")
       (filterAttrs (k: v: v != null) cfg.shellAliases)
   );
 
@@ -173,10 +173,10 @@ in
         # This file is read for all shells.
 
         # Only execute this file once per shell.
-        if [ -n "$__ETC_ZSHENV_SOURCED" ]; then return; fi
+        if [ -n "''${__ETC_ZSHENV_SOURCED-}" ]; then return; fi
         __ETC_ZSHENV_SOURCED=1
 
-        if [ -z "$__NIXOS_SET_ENVIRONMENT_DONE" ]; then
+        if [ -z "''${__NIXOS_SET_ENVIRONMENT_DONE-}" ]; then
             . ${config.system.build.setEnvironment}
         fi
 
@@ -184,7 +184,7 @@ in
 
         # Tell zsh how to find installed completions.
         for p in ''${(z)NIX_PROFILES}; do
-            fpath+=($p/share/zsh/site-functions $p/share/zsh/$ZSH_VERSION/functions $p/share/zsh/vendor-completions)
+            fpath=($p/share/zsh/site-functions $p/share/zsh/$ZSH_VERSION/functions $p/share/zsh/vendor-completions $fpath)
         done
 
         # Setup custom shell init stuff.
@@ -206,7 +206,7 @@ in
         ${zshStartupNotes}
 
         # Only execute this file once per shell.
-        if [ -n "$__ETC_ZPROFILE_SOURCED" ]; then return; fi
+        if [ -n "''${__ETC_ZPROFILE_SOURCED-}" ]; then return; fi
         __ETC_ZPROFILE_SOURCED=1
 
         # Setup custom login shell init stuff.
diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix
index e9299fb1b3adb..4e163901b0887 100644
--- a/nixos/modules/security/acme/default.nix
+++ b/nixos/modules/security/acme/default.nix
@@ -26,8 +26,8 @@ let
     Type = "oneshot";
     User = user;
     Group = mkDefault "acme";
-    UMask = 0022;
-    StateDirectoryMode = 750;
+    UMask = "0022";
+    StateDirectoryMode = "750";
     ProtectSystem = "strict";
     ReadWritePaths = [
       "/var/lib/acme"
@@ -62,9 +62,9 @@ let
     SystemCallArchitectures = "native";
     SystemCallFilter = [
       # 1. allow a reasonable set of syscalls
-      "@system-service"
+      "@system-service @resources"
       # 2. and deny unreasonable ones
-      "~@privileged @resources"
+      "~@privileged"
       # 3. then allow the required subset within denied groups
       "@chown"
     ];
@@ -85,7 +85,7 @@ let
     serviceConfig = commonServiceConfig // {
       StateDirectory = "acme/.minica";
       BindPaths = "/var/lib/acme/.minica:/tmp/ca";
-      UMask = 0077;
+      UMask = "0077";
     };
 
     # Working directory will be /tmp
@@ -243,7 +243,7 @@ let
 
       serviceConfig = commonServiceConfig // {
         Group = data.group;
-        UMask = 0027;
+        UMask = "0027";
 
         StateDirectory = "acme/${cert}";
 
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index c74f66d918295..21e1749d85032 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -392,6 +392,24 @@ let
         '';
       };
 
+      failDelay = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = lib.mdDoc ''
+            If enabled, this will replace the `FAIL_DELAY` setting from `login.defs`.
+            Change the delay on failure per-application.
+            '';
+        };
+
+        delay = mkOption {
+          default = 3000000;
+          type = types.int;
+          example = 1000000;
+          description = lib.mdDoc "The delay time (in microseconds) on failure.";
+        };
+      };
+
       gnupg = {
         enable = mkOption {
           type = types.bool;
@@ -526,11 +544,13 @@ let
           # We use try_first_pass the second time to avoid prompting password twice
           (optionalString (cfg.unixAuth &&
             (config.security.pam.enableEcryptfs
+              || config.security.pam.enableFscrypt
               || cfg.pamMount
               || cfg.enableKwallet
               || cfg.enableGnomeKeyring
               || cfg.googleAuthenticator.enable
               || cfg.gnupg.enable
+              || cfg.failDelay.enable
               || cfg.duoSecurity.enable))
             (
               ''
@@ -539,6 +559,9 @@ let
               optionalString config.security.pam.enableEcryptfs ''
                 auth optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so unwrap
               '' +
+              optionalString config.security.pam.enableFscrypt ''
+                auth optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so
+              '' +
               optionalString cfg.pamMount ''
                 auth optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive
               '' +
@@ -551,6 +574,9 @@ let
               optionalString cfg.gnupg.enable ''
                 auth optional ${pkgs.pam_gnupg}/lib/security/pam_gnupg.so ${optionalString cfg.gnupg.storeOnly " store-only"}
               '' +
+              optionalString cfg.failDelay.enable ''
+                auth optional ${pkgs.pam}/lib/security/pam_faildelay.so delay=${toString cfg.failDelay.delay}
+              '' +
               optionalString cfg.googleAuthenticator.enable ''
                 auth required ${pkgs.google-authenticator}/lib/security/pam_google_authenticator.so no_increment_hotp
               '' +
@@ -584,6 +610,9 @@ let
           optionalString config.security.pam.enableEcryptfs ''
             password optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so
           '' +
+          optionalString config.security.pam.enableFscrypt ''
+            password optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so
+          '' +
           optionalString cfg.pamMount ''
             password optional ${pkgs.pam_mount}/lib/security/pam_mount.so
           '' +
@@ -615,12 +644,12 @@ let
           optionalString cfg.setLoginUid ''
             session ${if config.boot.isContainer then "optional" else "required"} pam_loginuid.so
           '' +
-          optionalString cfg.ttyAudit.enable ''
-            session required ${pkgs.pam}/lib/security/pam_tty_audit.so
-                open_only=${toString cfg.ttyAudit.openOnly}
-                ${optionalString (cfg.ttyAudit.enablePattern != null) "enable=${cfg.ttyAudit.enablePattern}"}
-                ${optionalString (cfg.ttyAudit.disablePattern != null) "disable=${cfg.ttyAudit.disablePattern}"}
-          '' +
+          optionalString cfg.ttyAudit.enable (concatStringsSep " \\\n  " ([
+            "session required ${pkgs.pam}/lib/security/pam_tty_audit.so"
+          ] ++ optional cfg.ttyAudit.openOnly "open_only"
+          ++ optional (cfg.ttyAudit.enablePattern != null) "enable=${cfg.ttyAudit.enablePattern}"
+          ++ optional (cfg.ttyAudit.disablePattern != null) "disable=${cfg.ttyAudit.disablePattern}"
+          )) +
           optionalString cfg.makeHomeDir ''
             session required ${pkgs.pam}/lib/security/pam_mkhomedir.so silent skel=${config.security.pam.makeHomeDir.skelDirectory} umask=0077
           '' +
@@ -630,6 +659,14 @@ let
           optionalString config.security.pam.enableEcryptfs ''
             session optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so
           '' +
+          optionalString config.security.pam.enableFscrypt ''
+            # Work around https://github.com/systemd/systemd/issues/8598
+            # Skips the pam_fscrypt module for systemd-user sessions which do not have a password
+            # anyways.
+            # See also https://github.com/google/fscrypt/issues/95
+            session [success=1 default=ignore] pam_succeed_if.so service = systemd-user
+            session optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so
+          '' +
           optionalString cfg.pamMount ''
             session optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive
           '' +
@@ -1146,6 +1183,14 @@ in
     };
 
     security.pam.enableEcryptfs = mkEnableOption (lib.mdDoc "eCryptfs PAM module (mounting ecryptfs home directory on login)");
+    security.pam.enableFscrypt = mkEnableOption (lib.mdDoc ''
+      Enables fscrypt to automatically unlock directories with the user's login password.
+
+      This also enables a service at security.pam.services.fscrypt which is used by
+      fscrypt to verify the user's password when setting up a new protector. If you
+      use something other than pam_unix to verify user passwords, please remember to
+      adjust this PAM service.
+    '');
 
     users.motd = mkOption {
       default = null;
@@ -1170,6 +1215,7 @@ in
       ++ optionals config.security.pam.enableOTPW [ pkgs.otpw ]
       ++ optionals config.security.pam.oath.enable [ pkgs.oath-toolkit ]
       ++ optionals config.security.pam.p11.enable [ pkgs.pam_p11 ]
+      ++ optionals config.security.pam.enableFscrypt [ pkgs.fscrypt-experimental ]
       ++ optionals config.security.pam.u2f.enable [ pkgs.pam_u2f ];
 
     boot.supportedFilesystems = optionals config.security.pam.enableEcryptfs [ "ecryptfs" ];
@@ -1211,6 +1257,9 @@ in
            it complains "Cannot create session: Already running in a
            session". */
         runuser-l = { rootOK = true; unixAuth = false; };
+      } // optionalAttrs (config.security.pam.enableFscrypt) {
+        # Allow fscrypt to verify login passphrase
+        fscrypt = {};
       };
 
     security.apparmor.includes."abstractions/pam" = let
@@ -1275,6 +1324,9 @@ in
       optionalString config.security.pam.enableEcryptfs ''
         mr ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so,
       '' +
+      optionalString config.security.pam.enableFscrypt ''
+        mr ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so,
+      '' +
       optionalString (isEnabled (cfg: cfg.pamMount)) ''
         mr ${pkgs.pam_mount}/lib/security/pam_mount.so,
       '' +
diff --git a/nixos/modules/security/polkit.nix b/nixos/modules/security/polkit.nix
index 95a2d4d5377a5..f33898578b817 100644
--- a/nixos/modules/security/polkit.nix
+++ b/nixos/modules/security/polkit.nix
@@ -14,6 +14,8 @@ in
 
     security.polkit.enable = mkEnableOption (lib.mdDoc "polkit");
 
+    security.polkit.debug = mkEnableOption (lib.mdDoc "debug logs from polkit. This is required in order to see log messages from rule definitions.");
+
     security.polkit.extraConfig = mkOption {
       type = types.lines;
       default = "";
@@ -21,6 +23,7 @@ in
         ''
           /* Log authorization checks. */
           polkit.addRule(function(action, subject) {
+            // Make sure to set { security.polkit.debug = true; } in configuration.nix
             polkit.log("user " +  subject.user + " is attempting action " + action.id + " from PID " + subject.pid);
           });
 
@@ -58,6 +61,11 @@ in
 
     systemd.packages = [ pkgs.polkit.out ];
 
+    systemd.services.polkit.serviceConfig.ExecStart = [
+      ""
+      "${pkgs.polkit.out}/lib/polkit-1/polkitd ${optionalString (!cfg.debug) "--no-debug"}"
+    ];
+
     systemd.services.polkit.restartTriggers = [ config.system.path ];
     systemd.services.polkit.stopIfChanged = false;
 
diff --git a/nixos/modules/services/audio/navidrome.nix b/nixos/modules/services/audio/navidrome.nix
index d74835e220f74..e73828081d4bd 100644
--- a/nixos/modules/services/audio/navidrome.nix
+++ b/nixos/modules/services/audio/navidrome.nix
@@ -62,7 +62,7 @@ in {
         ProtectKernelModules = true;
         ProtectKernelTunables = true;
         SystemCallArchitectures = "native";
-        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
         RestrictRealtime = true;
         LockPersonality = true;
         MemoryDenyWriteExecute = true;
diff --git a/nixos/modules/services/audio/ympd.nix b/nixos/modules/services/audio/ympd.nix
index f14c81cdb8dde..811b81030efcf 100644
--- a/nixos/modules/services/audio/ympd.nix
+++ b/nixos/modules/services/audio/ympd.nix
@@ -29,7 +29,7 @@ in {
         };
 
         port = mkOption {
-          type = types.int;
+          type = types.port;
           default = config.services.mpd.network.port;
           defaultText = literalExpression "config.services.mpd.network.port";
           description = lib.mdDoc "The port where MPD is listening.";
diff --git a/nixos/modules/services/backup/bacula.nix b/nixos/modules/services/backup/bacula.nix
index d4f6ac8f5d37f..0acbf1b3eabba 100644
--- a/nixos/modules/services/backup/bacula.nix
+++ b/nixos/modules/services/backup/bacula.nix
@@ -314,7 +314,7 @@ in {
 
       port = mkOption {
         default = 9102;
-        type = types.int;
+        type = types.port;
         description = lib.mdDoc ''
           This specifies the port number on which the Client listens for
           Director connections. It must agree with the FDPort specified in
@@ -374,7 +374,7 @@ in {
 
       port = mkOption {
         default = 9103;
-        type = types.int;
+        type = types.port;
         description = lib.mdDoc ''
           Specifies port number on which the Storage daemon listens for
           Director connections.
@@ -451,7 +451,7 @@ in {
 
       port = mkOption {
         default = 9101;
-        type = types.int;
+        type = types.port;
         description = lib.mdDoc ''
           Specify the port (a positive integer) on which the Director daemon
           will listen for Bacula Console connections. This same port number
diff --git a/nixos/modules/services/backup/btrbk.nix b/nixos/modules/services/backup/btrbk.nix
index f1d58f597c255..b6eb68cc43f12 100644
--- a/nixos/modules/services/backup/btrbk.nix
+++ b/nixos/modules/services/backup/btrbk.nix
@@ -1,72 +1,74 @@
 { config, pkgs, lib, ... }:
 let
   inherit (lib)
+    concatLists
+    concatMap
     concatMapStringsSep
     concatStringsSep
     filterAttrs
-    flatten
     isAttrs
-    isString
     literalExpression
     mapAttrs'
     mapAttrsToList
     mkIf
     mkOption
     optionalString
-    partition
-    typeOf
+    sort
     types
     ;
 
-  cfg = config.services.btrbk;
-  sshEnabled = cfg.sshAccess != [ ];
-  serviceEnabled = cfg.instances != { };
-  attr2Lines = attr:
+  # The priority of an option or section.
+  # The configurations format are order-sensitive. Pairs are added as children of
+  # the last sections if possible, otherwise, they start a new section.
+  # We sort them in topological order:
+  # 1. Leaf pairs.
+  # 2. Sections that may contain (1).
+  # 3. Sections that may contain (1) or (2).
+  # 4. Etc.
+  prioOf = { name, value }:
+    if !isAttrs value then 0 # Leaf options.
+    else {
+      target = 1; # Contains: options.
+      subvolume = 2; # Contains: options, target.
+      volume = 3; # Contains: options, target, subvolume.
+    }.${name} or (throw "Unknow section '${name}'");
+
+  genConfig' = set: concatStringsSep "\n" (genConfig set);
+  genConfig = set:
     let
-      pairs = mapAttrsToList (name: value: { inherit name value; }) attr;
-      isSubsection = value:
-        if isAttrs value then true
-        else if isString value then false
-        else throw "invalid type in btrbk config ${typeOf value}";
-      sortedPairs = partition (x: isSubsection x.value) pairs;
+      pairs = mapAttrsToList (name: value: { inherit name value; }) set;
+      sortedPairs = sort (a: b: prioOf a < prioOf b) pairs;
     in
-    flatten (
-      # non subsections go first
-      (
-        map (pair: [ "${pair.name} ${pair.value}" ]) sortedPairs.wrong
-      )
-      ++ # subsections go last
-      (
-        map
-          (
-            pair:
-            mapAttrsToList
-              (
-                childname: value:
-                  [ "${pair.name} ${childname}" ] ++ (map (x: " " + x) (attr2Lines value))
-              )
-              pair.value
-          )
-          sortedPairs.right
-      )
-    )
-  ;
+      concatMap genPair sortedPairs;
+  genSection = sec: secName: value:
+    [ "${sec} ${secName}" ] ++ map (x: " " + x) (genConfig value);
+  genPair = { name, value }:
+    if !isAttrs value
+    then [ "${name} ${value}" ]
+    else concatLists (mapAttrsToList (genSection name) value);
+
   addDefaults = settings: { backend = "btrfs-progs-sudo"; } // settings;
-  mkConfigFile = settings: concatStringsSep "\n" (attr2Lines (addDefaults settings));
-  mkTestedConfigFile = name: settings:
-    let
-      configFile = pkgs.writeText "btrbk-${name}.conf" (mkConfigFile settings);
-    in
-    pkgs.runCommand "btrbk-${name}-tested.conf" { } ''
-      mkdir foo
-      cp ${configFile} $out
-      if (set +o pipefail; ${pkgs.btrbk}/bin/btrbk -c $out ls foo 2>&1 | grep $out);
-      then
-      echo btrbk configuration is invalid
-      cat $out
-      exit 1
-      fi;
+
+  mkConfigFile = name: settings: pkgs.writeTextFile {
+    name = "btrbk-${name}.conf";
+    text = genConfig' (addDefaults settings);
+    checkPhase = ''
+      set +e
+      ${pkgs.btrbk}/bin/btrbk -c $out dryrun
+      # According to btrbk(1), exit status 2 means parse error
+      # for CLI options or the config file.
+      if [[ $? == 2 ]]; then
+        echo "Btrbk configuration is invalid:"
+        cat $out
+        exit 1
+      fi
+      set -e
     '';
+  };
+
+  cfg = config.services.btrbk;
+  sshEnabled = cfg.sshAccess != [ ];
+  serviceEnabled = cfg.instances != { };
 in
 {
   meta.maintainers = with lib.maintainers; [ oxalica ];
@@ -196,7 +198,7 @@ in
       (
         name: instance: {
           name = "btrbk/${name}.conf";
-          value.source = mkTestedConfigFile name instance.settings;
+          value.source = mkConfigFile name instance.settings;
         }
       )
       cfg.instances;
diff --git a/nixos/modules/services/backup/duplicati.nix b/nixos/modules/services/backup/duplicati.nix
index 47f0b618c8d9c..007396ebfc9b4 100644
--- a/nixos/modules/services/backup/duplicati.nix
+++ b/nixos/modules/services/backup/duplicati.nix
@@ -12,7 +12,7 @@ in
 
       port = mkOption {
         default = 8200;
-        type = types.int;
+        type = types.port;
         description = lib.mdDoc ''
           Port serving the web interface
         '';
diff --git a/nixos/modules/services/blockchain/ethereum/erigon.nix b/nixos/modules/services/blockchain/ethereum/erigon.nix
new file mode 100644
index 0000000000000..9ff7df0b15e56
--- /dev/null
+++ b/nixos/modules/services/blockchain/ethereum/erigon.nix
@@ -0,0 +1,114 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+
+  cfg = config.services.erigon;
+
+  settingsFormat = pkgs.formats.toml { };
+  configFile = settingsFormat.generate "config.toml" cfg.settings;
+in {
+
+  options = {
+    services.erigon = {
+      enable = mkEnableOption (lib.mdDoc "Ethereum implementation on the efficiency frontier");
+
+      secretJwtPath = mkOption {
+        type = types.path;
+        description = lib.mdDoc ''
+          Path to the secret jwt used for the http api authentication.
+        '';
+        default = "";
+        example = "config.age.secrets.ERIGON_JWT.path";
+      };
+
+      settings = mkOption {
+        description = lib.mdDoc ''
+          Configuration for Erigon
+          Refer to <https://github.com/ledgerwatch/erigon#usage> for details on supported values.
+        '';
+
+        type = settingsFormat.type;
+
+        example = {
+          datadir = "/var/lib/erigon";
+          chain = "mainnet";
+          http = true;
+          "http.port" = 8545;
+          "http.api" = ["eth" "debug" "net" "trace" "web3" "erigon"];
+          ws = true;
+          port = 30303;
+          "authrpc.port" = 8551;
+          "torrent.port" = 42069;
+          "private.api.addr" = "localhost:9090";
+          "log.console.verbosity" = 3; # info
+        };
+
+        defaultText = literalExpression ''
+          {
+            datadir = "/var/lib/erigon";
+            chain = "mainnet";
+            http = true;
+            "http.port" = 8545;
+            "http.api" = ["eth" "debug" "net" "trace" "web3" "erigon"];
+            ws = true;
+            port = 30303;
+            "authrpc.port" = 8551;
+            "torrent.port" = 42069;
+            "private.api.addr" = "localhost:9090";
+            "log.console.verbosity" = 3; # info
+          }
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Default values are the same as in the binary, they are just written here for convenience.
+    services.erigon.settings = {
+      datadir = mkDefault "/var/lib/erigon";
+      chain = mkDefault "mainnet";
+      http = mkDefault true;
+      "http.port" = mkDefault 8545;
+      "http.api" = mkDefault ["eth" "debug" "net" "trace" "web3" "erigon"];
+      ws = mkDefault true;
+      port = mkDefault 30303;
+      "authrpc.port" = mkDefault 8551;
+      "torrent.port" = mkDefault 42069;
+      "private.api.addr" = mkDefault "localhost:9090";
+      "log.console.verbosity" = mkDefault 3; # info
+    };
+
+    systemd.services.erigon = {
+      description = "Erigon ethereum implemenntation";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        LoadCredential = "ERIGON_JWT:${cfg.secretJwtPath}";
+        ExecStart = "${pkgs.erigon}/bin/erigon --config ${configFile} --authrpc.jwtsecret=%d/ERIGON_JWT";
+        DynamicUser = true;
+        Restart = "on-failure";
+        StateDirectory = "erigon";
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectClock = true;
+        ProtectProc = "noaccess";
+        ProcSubset = "pid";
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        RestrictSUIDSGID = true;
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RemoveIPC = true;
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/blockchain/ethereum/lighthouse.nix b/nixos/modules/services/blockchain/ethereum/lighthouse.nix
new file mode 100644
index 0000000000000..db72c62d33044
--- /dev/null
+++ b/nixos/modules/services/blockchain/ethereum/lighthouse.nix
@@ -0,0 +1,313 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+
+  cfg = config.services.lighthouse;
+in {
+
+  options = {
+    services.lighthouse = {
+      beacon = mkOption {
+        description = lib.mdDoc "Beacon node";
+        default = {};
+        type = types.submodule {
+          options = {
+            enable = lib.mkEnableOption (lib.mdDoc "Lightouse Beacon node");
+
+            dataDir = mkOption {
+              type = types.str;
+              default = "/var/lib/lighthouse-beacon";
+              description = lib.mdDoc ''
+                Directory where data will be stored. Each chain will be stored under it's own specific subdirectory.
+              '';
+            };
+
+            address = mkOption {
+              type = types.str;
+              default = "0.0.0.0";
+              description = lib.mdDoc ''
+                Listen address of Beacon node.
+              '';
+            };
+
+            port = mkOption {
+              type = types.port;
+              default = 9000;
+              description = lib.mdDoc ''
+                Port number the Beacon node will be listening on.
+              '';
+            };
+
+            openFirewall = mkOption {
+              type = types.bool;
+              default = false;
+              description = lib.mdDoc ''
+                Open the port in the firewall
+              '';
+            };
+
+            disableDepositContractSync = mkOption {
+              type = types.bool;
+              default = false;
+              description = lib.mdDoc ''
+                Explictly disables syncing of deposit logs from the execution node.
+                This overrides any previous option that depends on it.
+                Useful if you intend to run a non-validating beacon node.
+              '';
+            };
+
+            execution = {
+              address = mkOption {
+                type = types.str;
+                default = "127.0.0.1";
+                description = lib.mdDoc ''
+                  Listen address for the execution layer.
+                '';
+              };
+
+              port = mkOption {
+                type = types.port;
+                default = 8551;
+                description = lib.mdDoc ''
+                  Port number the Beacon node will be listening on for the execution layer.
+                '';
+              };
+
+              jwtPath = mkOption {
+                type = types.str;
+                default = "";
+                description = lib.mdDoc ''
+                  Path for the jwt secret required to connect to the execution layer.
+                '';
+              };
+            };
+
+            http = {
+              enable = lib.mkEnableOption (lib.mdDoc "Beacon node http api");
+              port = mkOption {
+                type = types.port;
+                default = 5052;
+                description = lib.mdDoc ''
+                  Port number of Beacon node RPC service.
+                '';
+              };
+
+              address = mkOption {
+                type = types.str;
+                default = "127.0.0.1";
+                description = lib.mdDoc ''
+                  Listen address of Beacon node RPC service.
+                '';
+              };
+            };
+
+            metrics = {
+              enable = lib.mkEnableOption (lib.mdDoc "Beacon node prometheus metrics");
+              address = mkOption {
+                type = types.str;
+                default = "127.0.0.1";
+                description = lib.mdDoc ''
+                  Listen address of Beacon node metrics service.
+                '';
+              };
+
+              port = mkOption {
+                type = types.port;
+                default = 5054;
+                description = lib.mdDoc ''
+                  Port number of Beacon node metrics service.
+                '';
+              };
+            };
+
+            extraArgs = mkOption {
+              type = types.str;
+              description = lib.mdDoc ''
+                Additional arguments passed to the lighthouse beacon command.
+              '';
+              default = "";
+              example = "";
+            };
+          };
+        };
+      };
+
+      validator = mkOption {
+        description = lib.mdDoc "Validator node";
+        default = {};
+        type = types.submodule {
+          options = {
+            enable = mkOption {
+              type = types.bool;
+              default = false;
+              description = lib.mdDoc "Enable Lightouse Validator node.";
+            };
+
+            dataDir = mkOption {
+              type = types.str;
+              default = "/var/lib/lighthouse-validator";
+              description = lib.mdDoc ''
+                Directory where data will be stored. Each chain will be stored under it's own specific subdirectory.
+              '';
+            };
+
+            beaconNodes = mkOption {
+              type = types.listOf types.str;
+              default = ["http://localhost:5052"];
+              description = lib.mdDoc ''
+                Beacon nodes to connect to.
+              '';
+            };
+
+            metrics = {
+              enable = lib.mkEnableOption (lib.mdDoc "Validator node prometheus metrics");
+              address = mkOption {
+                type = types.str;
+                default = "127.0.0.1";
+                description = lib.mdDoc ''
+                  Listen address of Validator node metrics service.
+                '';
+              };
+
+              port = mkOption {
+                type = types.port;
+                default = 5056;
+                description = lib.mdDoc ''
+                  Port number of Validator node metrics service.
+                '';
+              };
+            };
+
+            extraArgs = mkOption {
+              type = types.str;
+              description = lib.mdDoc ''
+                Additional arguments passed to the lighthouse validator command.
+              '';
+              default = "";
+              example = "";
+            };
+          };
+        };
+      };
+
+      network = mkOption {
+        type = types.enum [ "mainnet" "prater" "goerli" "gnosis" "kiln" "ropsten" "sepolia" ];
+        default = "mainnet";
+        description = lib.mdDoc ''
+          The network to connect to. Mainnet is the default ethereum network.
+        '';
+      };
+
+      extraArgs = mkOption {
+        type = types.str;
+        description = lib.mdDoc ''
+          Additional arguments passed to every lighthouse command.
+        '';
+        default = "";
+        example = "";
+      };
+    };
+  };
+
+  config = mkIf (cfg.beacon.enable || cfg.validator.enable) {
+
+    environment.systemPackages = [ pkgs.lighthouse ] ;
+
+    networking.firewall = mkIf cfg.beacon.enable {
+      allowedTCPPorts = mkIf cfg.beacon.openFirewall [ cfg.beacon.port ];
+      allowedUDPPorts = mkIf cfg.beacon.openFirewall [ cfg.beacon.port ];
+    };
+
+
+    systemd.services.lighthouse-beacon = mkIf cfg.beacon.enable {
+      description = "Lighthouse beacon node (connect to P2P nodes and verify blocks)";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      script = ''
+        # make sure the chain data directory is created on first run
+        mkdir -p ${cfg.beacon.dataDir}/${cfg.network}
+
+        ${pkgs.lighthouse}/bin/lighthouse beacon_node \
+          --disable-upnp \
+          ${lib.optionalString cfg.beacon.disableDepositContractSync "--disable-deposit-contract-sync"} \
+          --port ${toString cfg.beacon.port} \
+          --listen-address ${cfg.beacon.address} \
+          --network ${cfg.network} \
+          --datadir ${cfg.beacon.dataDir}/${cfg.network} \
+          --execution-endpoint http://${cfg.beacon.execution.address}:${toString cfg.beacon.execution.port} \
+          --execution-jwt ''${CREDENTIALS_DIRECTORY}/LIGHTHOUSE_JWT \
+          ${lib.optionalString cfg.beacon.http.enable '' --http --http-address ${cfg.beacon.http.address} --http-port ${toString cfg.beacon.http.port}''} \
+          ${lib.optionalString cfg.beacon.metrics.enable '' --metrics --metrics-address ${cfg.beacon.metrics.address} --metrics-port ${toString cfg.beacon.metrics.port}''} \
+          ${cfg.extraArgs} ${cfg.beacon.extraArgs}
+      '';
+      serviceConfig = {
+        LoadCredential = "LIGHTHOUSE_JWT:${cfg.beacon.execution.jwtPath}";
+        DynamicUser = true;
+        Restart = "on-failure";
+        StateDirectory = "lighthouse-beacon";
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectClock = true;
+        ProtectProc = "noaccess";
+        ProcSubset = "pid";
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        RestrictSUIDSGID = true;
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RemoveIPC = true;
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
+      };
+    };
+
+    systemd.services.lighthouse-validator = mkIf cfg.validator.enable {
+      description = "Lighthouse validtor node (manages validators, using data obtained from the beacon node via a HTTP API)";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      script = ''
+        # make sure the chain data directory is created on first run
+        mkdir -p ${cfg.validator.dataDir}/${cfg.network}
+
+        ${pkgs.lighthouse}/bin/lighthouse validator_client \
+          --network ${cfg.network} \
+          --beacon-nodes ${lib.concatStringsSep "," cfg.validator.beaconNodes} \
+          --datadir ${cfg.validator.dataDir}/${cfg.network}
+          ${optionalString cfg.validator.metrics.enable ''--metrics --metrics-address ${cfg.validator.metrics.address} --metrics-port ${toString cfg.validator.metrics.port}''} \
+          ${cfg.extraArgs} ${cfg.validator.extraArgs}
+      '';
+
+      serviceConfig = {
+        Restart = "on-failure";
+        StateDirectory = "lighthouse-validator";
+        CapabilityBoundingSet = "";
+        DynamicUser = true;
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectClock = true;
+        ProtectProc = "noaccess";
+        ProcSubset = "pid";
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        RestrictSUIDSGID = true;
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/cluster/kubernetes/flannel.nix b/nixos/modules/services/cluster/kubernetes/flannel.nix
index 3ca85a8183c34..53003287fc9c9 100644
--- a/nixos/modules/services/cluster/kubernetes/flannel.nix
+++ b/nixos/modules/services/cluster/kubernetes/flannel.nix
@@ -12,7 +12,7 @@ in
 {
   ###### interface
   options.services.kubernetes.flannel = {
-    enable = mkEnableOption (lib.mdDoc "enable flannel networking");
+    enable = mkEnableOption (lib.mdDoc "flannel networking");
   };
 
   ###### implementation
diff --git a/nixos/modules/services/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix
index 5dcd18293488a..0898fee9bdb71 100644
--- a/nixos/modules/services/cluster/kubernetes/kubelet.nix
+++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix
@@ -177,8 +177,7 @@ in
 
     hostname = mkOption {
       description = lib.mdDoc "Kubernetes kubelet hostname override.";
-      default = config.networking.hostName;
-      defaultText = literalExpression "config.networking.hostName";
+      defaultText = literalExpression "config.networking.fqdnOrHostName";
       type = str;
     };
 
@@ -349,8 +348,8 @@ in
 
       boot.kernelModules = ["br_netfilter" "overlay"];
 
-      services.kubernetes.kubelet.hostname = with config.networking;
-        mkDefault (hostName + optionalString (domain != null) ".${domain}");
+      services.kubernetes.kubelet.hostname =
+        mkDefault config.networking.fqdnOrHostName;
 
       services.kubernetes.pki.certs = with top.lib; {
         kubelet = mkCert {
diff --git a/nixos/modules/services/computing/foldingathome/client.nix b/nixos/modules/services/computing/foldingathome/client.nix
index d8dd17a5cebb1..1229e5ac987e7 100644
--- a/nixos/modules/services/computing/foldingathome/client.nix
+++ b/nixos/modules/services/computing/foldingathome/client.nix
@@ -18,7 +18,7 @@ in
     '')
   ];
   options.services.foldingathome = {
-    enable = mkEnableOption (lib.mdDoc "Enable the Folding@home client");
+    enable = mkEnableOption (lib.mdDoc "Folding@home client");
 
     package = mkOption {
       type = types.package;
diff --git a/nixos/modules/services/continuous-integration/github-runner.nix b/nixos/modules/services/continuous-integration/github-runner.nix
index 2ece75722a1d3..24d02c931a4ae 100644
--- a/nixos/modules/services/continuous-integration/github-runner.nix
+++ b/nixos/modules/services/continuous-integration/github-runner.nix
@@ -1,396 +1,23 @@
-{ config, pkgs, lib, ... }:
+{ config
+, pkgs
+, lib
+, ...
+}@args:
+
 with lib;
+
 let
   cfg = config.services.github-runner;
-  svcName = "github-runner";
-  systemdDir = "${svcName}/${cfg.name}";
-  # %t: Runtime directory root (usually /run); see systemd.unit(5)
-  runtimeDir = "%t/${systemdDir}";
-  # %S: State directory root (usually /var/lib); see systemd.unit(5)
-  stateDir = "%S/${systemdDir}";
-  # %L: Log directory root (usually /var/log); see systemd.unit(5)
-  logsDir = "%L/${systemdDir}";
-  # Name of file stored in service state directory
-  currentConfigTokenFilename = ".current-token";
 in
-{
-  options.services.github-runner = {
-    enable = mkOption {
-      default = false;
-      example = true;
-      description = lib.mdDoc ''
-        Whether to enable GitHub Actions runner.
-
-        Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here:
-        [About self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners).
-      '';
-      type = lib.types.bool;
-    };
-
-    url = mkOption {
-      type = types.str;
-      description = lib.mdDoc ''
-        Repository to add the runner to.
-
-        Changing this option triggers a new runner registration.
-
-        IMPORTANT: If your token is org-wide (not per repository), you need to
-        provide a github org link, not a single repository, so do it like this
-        `https://github.com/nixos`, not like this
-        `https://github.com/nixos/nixpkgs`.
-        Otherwise, you are going to get a `404 NotFound`
-        from `POST https://api.github.com/actions/runner-registration`
-        in the configure script.
-      '';
-      example = "https://github.com/nixos/nixpkgs";
-    };
-
-    tokenFile = mkOption {
-      type = types.path;
-      description = lib.mdDoc ''
-        The full path to a file which contains either a runner registration token or a
-        personal access token (PAT).
-        The file should contain exactly one line with the token without any newline.
-        If a registration token is given, it can be used to re-register a runner of the same
-        name but is time-limited. If the file contains a PAT, the service creates a new
-        registration token on startup as needed. Make sure the PAT has a scope of
-        `admin:org` for organization-wide registrations or a scope of
-        `repo` for a single repository.
-
-        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 = lib.mdDoc ''
-        Name of the runner to configure. Defaults to the hostname.
-
-        Changing this option triggers a new runner registration.
-      '';
-      example = "nixos";
-      default = config.networking.hostName;
-      defaultText = literalExpression "config.networking.hostName";
-    };
-
-    runnerGroup = mkOption {
-      type = types.nullOr types.str;
-      description = lib.mdDoc ''
-        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 = lib.mdDoc ''
-        Extra labels in addition to the default (`["self-hosted", "Linux", "X64"]`).
 
-        Changing this option triggers a new runner registration.
-      '';
-      example = literalExpression ''[ "nixos" ]'';
-      default = [ ];
-    };
-
-    replace = mkOption {
-      type = types.bool;
-      description = lib.mdDoc ''
-        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 = lib.mdDoc ''
-        Extra packages to add to `PATH` of the service to make them available to workflows.
-      '';
-      default = [ ];
-    };
-
-    package = mkOption {
-      type = types.package;
-      description = lib.mdDoc ''
-        Which github-runner derivation to use.
-      '';
-      default = pkgs.github-runner;
-      defaultText = literalExpression "pkgs.github-runner";
-    };
-
-    ephemeral = mkOption {
-      type = types.bool;
-      description = lib.mdDoc ''
-        If enabled, causes the following behavior:
-
-        - Passes the `--ephemeral` flag to the runner configuration script
-        - De-registers and stops the runner with GitHub after it has processed one job
-        - On stop, systemd wipes the runtime directory (this always happens, even without using the ephemeral option)
-        - Restarts the service after its successful exit
-        - On start, wipes the state directory and configures a new runner
-
-        You should only enable this option if `tokenFile` points to a file which contains a
-        personal access token (PAT). If you're using the option with a registration token, restarting the
-        service will fail as soon as the registration token expired.
-      '';
-      default = false;
-    };
-  };
+{
+  options.services.github-runner = import ./github-runner/options.nix (args // {
+    # Users don't need to specify options.services.github-runner.name; it will default
+    # to the hostname.
+    includeNameDefault = true;
+  });
 
   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 = stateDir;
-      };
-
-      path = (with pkgs; [
-        bash
-        coreutils
-        git
-        gnutar
-        gzip
-      ]) ++ [
-        config.nix.package
-      ] ++ cfg.extraPackages;
-
-      serviceConfig = rec {
-        ExecStart = "${cfg.package}/bin/Runner.Listener run --startuptype service";
-
-        # Does the following, sequentially:
-        # - If the module configuration or the token has changed, purge the state directory,
-        #   and create the current and the new token file with the contents of the configured
-        #   token. While both files have the same content, only the later is accessible by
-        #   the service user.
-        # - Configure the runner using the new token file. When finished, delete it.
-        # - Set up the directory structure by creating the necessary symlinks.
-        ExecStartPre =
-          let
-            # Wrapper script which expects the full path of the state, runtime and logs
-            # directory as arguments. Overrides the respective systemd variables to provide
-            # unambiguous directory names. This becomes relevant, for example, if the
-            # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory=
-            # to contain more than one directory. This causes systemd to set the respective
-            # environment variables with the path of all of the given directories, separated
-            # by a colon.
-            writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" ''
-              set -euo pipefail
-
-              STATE_DIRECTORY="$1"
-              RUNTIME_DIRECTORY="$2"
-              LOGS_DIRECTORY="$3"
-
-              ${lines}
-            '';
-            runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" "ephemeral" ] cfg;
-            newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
-            currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
-            newConfigTokenPath= "$STATE_DIRECTORY/.new-token";
-            currentConfigTokenPath = "$STATE_DIRECTORY/${currentConfigTokenFilename}";
-
-            runnerCredFiles = [
-              ".credentials"
-              ".credentials_rsaparams"
-              ".runner"
-            ];
-            unconfigureRunner = writeScript "unconfigure" ''
-              copy_tokens() {
-                # Copy the configured token file to the state dir and allow the service user to read the file
-                install --mode=666 ${escapeShellArg cfg.tokenFile} "${newConfigTokenPath}"
-                # Also copy current file to allow for a diff on the next start
-                install --mode=600 ${escapeShellArg cfg.tokenFile} "${currentConfigTokenPath}"
-              }
-
-              clean_state() {
-                find "$STATE_DIRECTORY/" -mindepth 1 -delete
-                copy_tokens
-              }
-
-              diff_config() {
-                changed=0
-
-                # Check for module config changes
-                [[ -f "${currentConfigPath}" ]] \
-                  && ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 \
-                  || changed=1
-
-                # Also check the content of the token file
-                [[ -f "${currentConfigTokenPath}" ]] \
-                  && ${pkgs.diffutils}/bin/diff -q "${currentConfigTokenPath}" ${escapeShellArg cfg.tokenFile} >/dev/null 2>&1 \
-                  || changed=1
-
-                # If the config has changed, remove old state and copy tokens
-                if [[ "$changed" -eq 1 ]]; then
-                  echo "Config has changed, removing old runner state."
-                  echo "The old runner will still appear in the GitHub Actions UI." \
-                       "You have to remove it manually."
-                  clean_state
-                fi
-              }
-
-              if [[ "${optionalString cfg.ephemeral "1"}" ]]; then
-                # In ephemeral mode, we always want to start with a clean state
-                clean_state
-              elif [[ "$(ls -A "$STATE_DIRECTORY")" ]]; then
-                # There are state files from a previous run; diff them to decide if we need a new registration
-                diff_config
-              else
-                # The state directory is entirely empty which indicates a first start
-                copy_tokens
-              fi
-            '';
-            configureRunner = writeScript "configure" ''
-              if [[ -e "${newConfigTokenPath}" ]]; then
-                echo "Configuring GitHub Actions Runner"
-
-                args=(
-                  --unattended
-                  --disableupdate
-                  --work "$RUNTIME_DIRECTORY"
-                  --url ${escapeShellArg cfg.url}
-                  --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)}
-                  --name ${escapeShellArg cfg.name}
-                  ${optionalString cfg.replace "--replace"}
-                  ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
-                  ${optionalString cfg.ephemeral "--ephemeral"}
-                )
-
-                # If the token file contains a PAT (i.e., it starts with "ghp_"), we have to use the --pat option,
-                # if it is not a PAT, we assume it contains a registration token and use the --token option
-                token=$(<"${newConfigTokenPath}")
-                if [[ "$token" =~ ^ghp_* ]]; then
-                  args+=(--pat "$token")
-                else
-                  args+=(--token "$token")
-                fi
-
-                ${cfg.package}/bin/config.sh "''${args[@]}"
-
-                # Move the automatically created _diag dir to the logs dir
-                mkdir -p  "$STATE_DIRECTORY/_diag"
-                cp    -r  "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/"
-                rm    -rf "$STATE_DIRECTORY/_diag/"
-
-                # Cleanup token from config
-                rm "${newConfigTokenPath}"
-
-                # Symlink to new config
-                ln -s '${newConfigPath}' "${currentConfigPath}"
-              fi
-            '';
-            setupRuntimeDir = writeScript "setup-runtime-dirs" ''
-              # Link _diag dir
-              ln -s "$LOGS_DIRECTORY" "$RUNTIME_DIRECTORY/_diag"
-
-              # Link the runner credentials to the runtime dir
-              ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$RUNTIME_DIRECTORY/"
-            '';
-          in
-          map (x: "${x} ${escapeShellArgs [ stateDir runtimeDir logsDir ]}") [
-            "+${unconfigureRunner}" # runs as root
-            configureRunner
-            setupRuntimeDir
-          ];
-
-        # If running in ephemeral mode, restart the service on-exit (i.e., successful de-registration of the runner)
-        # to trigger a fresh registration.
-        Restart = if cfg.ephemeral then "on-success" else "no";
-
-        # Contains _diag
-        LogsDirectory = [ systemdDir ];
-        # Default RUNNER_ROOT which contains ephemeral Runner data
-        RuntimeDirectory = [ systemdDir ];
-        # Home of persistent runner data, e.g., credentials
-        StateDirectory = [ systemdDir ];
-        StateDirectoryMode = "0700";
-        WorkingDirectory = runtimeDir;
-
-        InaccessiblePaths = [
-          # Token file path given in the configuration, if visible to the service
-          "-${cfg.tokenFile}"
-          # Token file in the state directory
-          "${stateDir}/${currentConfigTokenFilename}"
-        ];
-
-        # By default, use a dynamically allocated user
-        DynamicUser = true;
-
-        KillSignal = "SIGINT";
-
-        # Hardening (may overlap with DynamicUser=)
-        # The following options are only for optimizing:
-        # systemd-analyze security github-runner
-        AmbientCapabilities = "";
-        CapabilityBoundingSet = "";
-        # ProtectClock= adds DeviceAllow=char-rtc r
-        DeviceAllow = "";
-        NoNewPrivileges = true;
-        PrivateDevices = true;
-        PrivateMounts = true;
-        PrivateTmp = true;
-        PrivateUsers = true;
-        ProtectClock = true;
-        ProtectControlGroups = true;
-        ProtectHome = true;
-        ProtectHostname = true;
-        ProtectKernelLogs = true;
-        ProtectKernelModules = true;
-        ProtectKernelTunables = true;
-        ProtectSystem = "strict";
-        RemoveIPC = true;
-        RestrictNamespaces = true;
-        RestrictRealtime = true;
-        RestrictSUIDSGID = true;
-        UMask = "0066";
-        ProtectProc = "invisible";
-        SystemCallFilter = [
-          "~@clock"
-          "~@cpu-emulation"
-          "~@module"
-          "~@mount"
-          "~@obsolete"
-          "~@raw-io"
-          "~@reboot"
-          "~capset"
-          "~setdomainname"
-          "~sethostname"
-        ];
-        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ];
-
-        # Needs network access
-        PrivateNetwork = false;
-        # Cannot be true due to Node
-        MemoryDenyWriteExecute = false;
-
-        # The more restrictive "pid" option makes `nix` commands in CI emit
-        # "GC Warning: Couldn't read /proc/stat"
-        # You may want to set this to "pid" if not using `nix` commands
-        ProcSubset = "all";
-        # Coverage programs for compiled code such as `cargo-tarpaulin` disable
-        # ASLR (address space layout randomization) which requires the
-        # `personality` syscall
-        # You may want to set this to `true` if not using coverage tooling on
-        # compiled code
-        LockPersonality = false;
-      };
-    };
+    services.github-runners.${cfg.name} = cfg;
   };
 }
diff --git a/nixos/modules/services/continuous-integration/github-runner/options.nix b/nixos/modules/services/continuous-integration/github-runner/options.nix
new file mode 100644
index 0000000000000..796b5a7f1175f
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/github-runner/options.nix
@@ -0,0 +1,173 @@
+{ config
+, lib
+, pkgs
+, includeNameDefault
+, ...
+}:
+
+with lib;
+
+{
+  enable = mkOption {
+    default = false;
+    example = true;
+    description = lib.mdDoc ''
+      Whether to enable GitHub Actions runner.
+
+      Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here:
+      [About self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners).
+    '';
+    type = lib.types.bool;
+  };
+
+  url = mkOption {
+    type = types.str;
+    description = lib.mdDoc ''
+      Repository to add the runner to.
+
+      Changing this option triggers a new runner registration.
+
+      IMPORTANT: If your token is org-wide (not per repository), you need to
+      provide a github org link, not a single repository, so do it like this
+      `https://github.com/nixos`, not like this
+      `https://github.com/nixos/nixpkgs`.
+      Otherwise, you are going to get a `404 NotFound`
+      from `POST https://api.github.com/actions/runner-registration`
+      in the configure script.
+    '';
+    example = "https://github.com/nixos/nixpkgs";
+  };
+
+  tokenFile = mkOption {
+    type = types.path;
+    description = lib.mdDoc ''
+      The full path to a file which contains either a runner registration token or a
+      (fine-grained) personal access token (PAT).
+      The file should contain exactly one line with the token without any newline.
+      If a registration token is given, it can be used to re-register a runner of the same
+      name but is time-limited. If the file contains a PAT, the service creates a new
+      registration token on startup as needed. Make sure the PAT has a scope of
+      `admin:org` for organization-wide registrations or a scope of
+      `repo` for a single repository. Fine-grained PATs need read and write permission
+      to the "Adminstration" resources.
+
+      Changing this option or the file's content triggers a new runner registration.
+    '';
+    example = "/run/secrets/github-runner/nixos.token";
+  };
+
+  name = let
+    # Same pattern as for `networking.hostName`
+    baseType = types.strMatching "^$|^[[:alnum:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$";
+  in mkOption {
+    type = if includeNameDefault then baseType else types.nullOr baseType;
+    description = lib.mdDoc ''
+      Name of the runner to configure. Defaults to the hostname.
+
+      Changing this option triggers a new runner registration.
+    '';
+    example = "nixos";
+  } // (if includeNameDefault then {
+    default = config.networking.hostName;
+    defaultText = literalExpression "config.networking.hostName";
+  } else {
+    default = null;
+  });
+
+  runnerGroup = mkOption {
+    type = types.nullOr types.str;
+    description = lib.mdDoc ''
+      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 = lib.mdDoc ''
+      Extra labels in addition to the default (`["self-hosted", "Linux", "X64"]`).
+
+      Changing this option triggers a new runner registration.
+    '';
+    example = literalExpression ''[ "nixos" ]'';
+    default = [ ];
+  };
+
+  replace = mkOption {
+    type = types.bool;
+    description = lib.mdDoc ''
+      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 = lib.mdDoc ''
+      Extra packages to add to `PATH` of the service to make them available to workflows.
+    '';
+    default = [ ];
+  };
+
+  extraEnvironment = mkOption {
+    type = types.attrs;
+    description = lib.mdDoc ''
+      Extra environment variables to set for the runner, as an attrset.
+    '';
+    example = {
+      GIT_CONFIG = "/path/to/git/config";
+    };
+    default = {};
+  };
+
+  serviceOverrides = mkOption {
+    type = types.attrs;
+    description = lib.mdDoc ''
+      Overrides for the systemd service. Can be used to adjust the sandboxing options.
+    '';
+    example = {
+      ProtectHome = false;
+    };
+    default = {};
+  };
+
+  package = mkOption {
+    type = types.package;
+    description = lib.mdDoc ''
+      Which github-runner derivation to use.
+    '';
+    default = pkgs.github-runner;
+    defaultText = literalExpression "pkgs.github-runner";
+  };
+
+  ephemeral = mkOption {
+    type = types.bool;
+    description = lib.mdDoc ''
+      If enabled, causes the following behavior:
+
+      - Passes the `--ephemeral` flag to the runner configuration script
+      - De-registers and stops the runner with GitHub after it has processed one job
+      - On stop, systemd wipes the runtime directory (this always happens, even without using the ephemeral option)
+      - Restarts the service after its successful exit
+      - On start, wipes the state directory and configures a new runner
+
+      You should only enable this option if `tokenFile` points to a file which contains a
+      personal access token (PAT). If you're using the option with a registration token, restarting the
+      service will fail as soon as the registration token expired.
+    '';
+    default = false;
+  };
+
+  user = mkOption {
+    type = types.nullOr types.str;
+    description = lib.mdDoc ''
+      User under which to run the service. If null, will use a systemd dynamic user.
+    '';
+    default = null;
+    defaultText = literalExpression "username";
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/github-runner/service.nix b/nixos/modules/services/continuous-integration/github-runner/service.nix
new file mode 100644
index 0000000000000..cd81631582f91
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/github-runner/service.nix
@@ -0,0 +1,257 @@
+{ config
+, lib
+, pkgs
+
+, cfg ? config.services.github-runner
+, svcName
+
+, systemdDir ? "${svcName}/${cfg.name}"
+  # %t: Runtime directory root (usually /run); see systemd.unit(5)
+, runtimeDir ? "%t/${systemdDir}"
+  # %S: State directory root (usually /var/lib); see systemd.unit(5)
+, stateDir ? "%S/${systemdDir}"
+  # %L: Log directory root (usually /var/log); see systemd.unit(5)
+, logsDir ? "%L/${systemdDir}"
+  # Name of file stored in service state directory
+, currentConfigTokenFilename ? ".current-token"
+
+, ...
+}:
+
+with lib;
+
+{
+  description = "GitHub Actions runner";
+
+  wantedBy = [ "multi-user.target" ];
+  wants = [ "network-online.target" ];
+  after = [ "network.target" "network-online.target" ];
+
+  environment = {
+    HOME = runtimeDir;
+    RUNNER_ROOT = stateDir;
+  } // cfg.extraEnvironment;
+
+  path = (with pkgs; [
+    bash
+    coreutils
+    git
+    gnutar
+    gzip
+  ]) ++ [
+    config.nix.package
+  ] ++ cfg.extraPackages;
+
+  serviceConfig = rec {
+    ExecStart = "${cfg.package}/bin/Runner.Listener run --startuptype service";
+
+    # Does the following, sequentially:
+    # - If the module configuration or the token has changed, purge the state directory,
+    #   and create the current and the new token file with the contents of the configured
+    #   token. While both files have the same content, only the later is accessible by
+    #   the service user.
+    # - Configure the runner using the new token file. When finished, delete it.
+    # - Set up the directory structure by creating the necessary symlinks.
+    ExecStartPre =
+      let
+        # Wrapper script which expects the full path of the state, runtime and logs
+        # directory as arguments. Overrides the respective systemd variables to provide
+        # unambiguous directory names. This becomes relevant, for example, if the
+        # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory=
+        # to contain more than one directory. This causes systemd to set the respective
+        # environment variables with the path of all of the given directories, separated
+        # by a colon.
+        writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" ''
+          set -euo pipefail
+
+          STATE_DIRECTORY="$1"
+          RUNTIME_DIRECTORY="$2"
+          LOGS_DIRECTORY="$3"
+
+          ${lines}
+        '';
+        runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" "ephemeral" ] cfg;
+        newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
+        currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
+        newConfigTokenPath= "$STATE_DIRECTORY/.new-token";
+        currentConfigTokenPath = "$STATE_DIRECTORY/${currentConfigTokenFilename}";
+
+        runnerCredFiles = [
+          ".credentials"
+          ".credentials_rsaparams"
+          ".runner"
+        ];
+        unconfigureRunner = writeScript "unconfigure" ''
+          copy_tokens() {
+            # Copy the configured token file to the state dir and allow the service user to read the file
+            install --mode=666 ${escapeShellArg cfg.tokenFile} "${newConfigTokenPath}"
+            # Also copy current file to allow for a diff on the next start
+            install --mode=600 ${escapeShellArg cfg.tokenFile} "${currentConfigTokenPath}"
+          }
+          clean_state() {
+            find "$STATE_DIRECTORY/" -mindepth 1 -delete
+            copy_tokens
+          }
+          diff_config() {
+            changed=0
+            # Check for module config changes
+            [[ -f "${currentConfigPath}" ]] \
+              && ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 \
+              || changed=1
+            # Also check the content of the token file
+            [[ -f "${currentConfigTokenPath}" ]] \
+              && ${pkgs.diffutils}/bin/diff -q "${currentConfigTokenPath}" ${escapeShellArg cfg.tokenFile} >/dev/null 2>&1 \
+              || changed=1
+            # If the config has changed, remove old state and copy tokens
+            if [[ "$changed" -eq 1 ]]; then
+              echo "Config has changed, removing old runner state."
+              echo "The old runner will still appear in the GitHub Actions UI." \
+                   "You have to remove it manually."
+              clean_state
+            fi
+          }
+          if [[ "${optionalString cfg.ephemeral "1"}" ]]; then
+            # In ephemeral mode, we always want to start with a clean state
+            clean_state
+          elif [[ "$(ls -A "$STATE_DIRECTORY")" ]]; then
+            # There are state files from a previous run; diff them to decide if we need a new registration
+            diff_config
+          else
+            # The state directory is entirely empty which indicates a first start
+            copy_tokens
+          fi        '';
+        configureRunner = writeScript "configure" ''
+          if [[ -e "${newConfigTokenPath}" ]]; then
+            echo "Configuring GitHub Actions Runner"
+            args=(
+              --unattended
+              --disableupdate
+              --work "$RUNTIME_DIRECTORY"
+              --url ${escapeShellArg cfg.url}
+              --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)}
+              --name ${escapeShellArg cfg.name}
+              ${optionalString cfg.replace "--replace"}
+              ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
+              ${optionalString cfg.ephemeral "--ephemeral"}
+            )
+            # If the token file contains a PAT (i.e., it starts with "ghp_" or "github_pat_"), we have to use the --pat option,
+            # if it is not a PAT, we assume it contains a registration token and use the --token option
+            token=$(<"${newConfigTokenPath}")
+            if [[ "$token" =~ ^ghp_* ]] || [[ "$token" =~ ^github_pat_* ]]; then
+              args+=(--pat "$token")
+            else
+              args+=(--token "$token")
+            fi
+            ${cfg.package}/bin/config.sh "''${args[@]}"
+            # Move the automatically created _diag dir to the logs dir
+            mkdir -p  "$STATE_DIRECTORY/_diag"
+            cp    -r  "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/"
+            rm    -rf "$STATE_DIRECTORY/_diag/"
+            # Cleanup token from config
+            rm "${newConfigTokenPath}"
+            # Symlink to new config
+            ln -s '${newConfigPath}' "${currentConfigPath}"
+          fi
+        '';
+        setupRuntimeDir = writeScript "setup-runtime-dirs" ''
+          # Link _diag dir
+          ln -s "$LOGS_DIRECTORY" "$RUNTIME_DIRECTORY/_diag"
+
+          # Link the runner credentials to the runtime dir
+          ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$RUNTIME_DIRECTORY/"
+        '';
+      in
+        map (x: "${x} ${escapeShellArgs [ stateDir runtimeDir logsDir ]}") [
+          "+${unconfigureRunner}" # runs as root
+          configureRunner
+          setupRuntimeDir
+        ];
+
+    # If running in ephemeral mode, restart the service on-exit (i.e., successful de-registration of the runner)
+    # to trigger a fresh registration.
+    Restart = if cfg.ephemeral then "on-success" else "no";
+    # If the runner exits with `ReturnCode.RetryableError = 2`, always restart the service:
+    # https://github.com/actions/runner/blob/40ed7f8/src/Runner.Common/Constants.cs#L146
+    RestartForceExitStatus = [ 2 ];
+
+    # Contains _diag
+    LogsDirectory = [ systemdDir ];
+    # Default RUNNER_ROOT which contains ephemeral Runner data
+    RuntimeDirectory = [ systemdDir ];
+    # Home of persistent runner data, e.g., credentials
+    StateDirectory = [ systemdDir ];
+    StateDirectoryMode = "0700";
+    WorkingDirectory = runtimeDir;
+
+    InaccessiblePaths = [
+      # Token file path given in the configuration, if visible to the service
+      "-${cfg.tokenFile}"
+      # Token file in the state directory
+      "${stateDir}/${currentConfigTokenFilename}"
+    ];
+
+    KillSignal = "SIGINT";
+
+    # Hardening (may overlap with DynamicUser=)
+    # The following options are only for optimizing:
+    # systemd-analyze security github-runner
+    AmbientCapabilities = "";
+    CapabilityBoundingSet = "";
+    # ProtectClock= adds DeviceAllow=char-rtc r
+    DeviceAllow = "";
+    NoNewPrivileges = true;
+    PrivateDevices = true;
+    PrivateMounts = true;
+    PrivateTmp = true;
+    PrivateUsers = true;
+    ProtectClock = true;
+    ProtectControlGroups = true;
+    ProtectHome = true;
+    ProtectHostname = true;
+    ProtectKernelLogs = true;
+    ProtectKernelModules = true;
+    ProtectKernelTunables = true;
+    ProtectSystem = "strict";
+    RemoveIPC = true;
+    RestrictNamespaces = true;
+    RestrictRealtime = true;
+    RestrictSUIDSGID = true;
+    UMask = "0066";
+    ProtectProc = "invisible";
+    SystemCallFilter = [
+      "~@clock"
+      "~@cpu-emulation"
+      "~@module"
+      "~@mount"
+      "~@obsolete"
+      "~@raw-io"
+      "~@reboot"
+      "~capset"
+      "~setdomainname"
+      "~sethostname"
+    ];
+    RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ];
+
+    # Needs network access
+    PrivateNetwork = false;
+    # Cannot be true due to Node
+    MemoryDenyWriteExecute = false;
+
+    # The more restrictive "pid" option makes `nix` commands in CI emit
+    # "GC Warning: Couldn't read /proc/stat"
+    # You may want to set this to "pid" if not using `nix` commands
+    ProcSubset = "all";
+    # Coverage programs for compiled code such as `cargo-tarpaulin` disable
+    # ASLR (address space layout randomization) which requires the
+    # `personality` syscall
+    # You may want to set this to `true` if not using coverage tooling on
+    # compiled code
+    LockPersonality = false;
+
+    # Note that this has some interactions with the User setting; so you may
+    # want to consult the systemd docs if using both.
+    DynamicUser = true;
+  } // (
+    lib.optionalAttrs (cfg.user != null) { User = cfg.user; }
+  ) // cfg.serviceOverrides;
+}
diff --git a/nixos/modules/services/continuous-integration/github-runners.nix b/nixos/modules/services/continuous-integration/github-runners.nix
new file mode 100644
index 0000000000000..78b57f9c7a256
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/github-runners.nix
@@ -0,0 +1,56 @@
+{ config
+, pkgs
+, lib
+, ...
+}@args:
+
+with lib;
+
+let
+  cfg = config.services.github-runners;
+
+in
+
+{
+  options.services.github-runners = mkOption {
+    default = {};
+    type = with types; attrsOf (submodule { options = import ./github-runner/options.nix (args // {
+      # services.github-runners.${name}.name doesn't have a default; it falls back to ${name} below.
+      includeNameDefault = false;
+    }); });
+    example = {
+      runner1 = {
+        enable = true;
+        url = "https://github.com/owner/repo";
+        name = "runner1";
+        tokenFile = "/secrets/token1";
+      };
+
+      runner2 = {
+        enable = true;
+        url = "https://github.com/owner/repo";
+        name = "runner2";
+        tokenFile = "/secrets/token2";
+      };
+    };
+    description = lib.mdDoc ''
+      Multiple GitHub Runners.
+    '';
+  };
+
+  config = {
+    systemd.services = flip mapAttrs' cfg (n: v:
+      let
+        svcName = "github-runner-${n}";
+      in
+        nameValuePair svcName
+        (import ./github-runner/service.nix (args // {
+          inherit svcName;
+          cfg = v // {
+            name = if v.name != null then v.name else n;
+          };
+          systemdDir = "github-runner/${n}";
+        }))
+    );
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/gocd-server/default.nix b/nixos/modules/services/continuous-integration/gocd-server/default.nix
index 80e819979fbfb..25c16a5c721ce 100644
--- a/nixos/modules/services/continuous-integration/gocd-server/default.nix
+++ b/nixos/modules/services/continuous-integration/gocd-server/default.nix
@@ -106,6 +106,8 @@ in {
           "-Dcruise.config.file=${cfg.workDir}/conf/cruise-config.xml"
           "-Dcruise.server.port=${toString cfg.port}"
           "-Dcruise.server.ssl.port=${toString cfg.sslPort}"
+          "--add-opens=java.base/java.lang=ALL-UNNAMED"
+          "--add-opens=java.base/java.util=ALL-UNNAMED"
         ];
         defaultText = literalExpression ''
           [
@@ -119,6 +121,8 @@ in {
             "-Dcruise.config.file=''${config.${opt.workDir}}/conf/cruise-config.xml"
             "-Dcruise.server.port=''${toString config.${opt.port}}"
             "-Dcruise.server.ssl.port=''${toString config.${opt.sslPort}}"
+            "--add-opens=java.base/java.lang=ALL-UNNAMED"
+            "--add-opens=java.base/java.util=ALL-UNNAMED"
           ]
         '';
 
@@ -199,7 +203,7 @@ in {
         ${pkgs.git}/bin/git config --global --add http.sslCAinfo /etc/ssl/certs/ca-certificates.crt
         ${pkgs.jre}/bin/java -server ${concatStringsSep " " cfg.startupOptions} \
                                ${concatStringsSep " " cfg.extraOptions}  \
-                              -jar ${pkgs.gocd-server}/go-server/go.jar
+                              -jar ${pkgs.gocd-server}/go-server/lib/go.jar
       '';
 
       serviceConfig = {
diff --git a/nixos/modules/services/continuous-integration/jenkins/default.nix b/nixos/modules/services/continuous-integration/jenkins/default.nix
index 6cd5718f42276..a9a587b41e881 100644
--- a/nixos/modules/services/continuous-integration/jenkins/default.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/default.nix
@@ -87,8 +87,8 @@ in {
       };
 
       packages = mkOption {
-        default = [ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ];
-        defaultText = literalExpression "[ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ]";
+        default = [ pkgs.stdenv pkgs.git pkgs.jdk17 config.programs.ssh.package pkgs.nix ];
+        defaultText = literalExpression "[ pkgs.stdenv pkgs.git pkgs.jdk17 config.programs.ssh.package pkgs.nix ]";
         type = types.listOf types.package;
         description = lib.mdDoc ''
           Packages to add to PATH for the jenkins process.
@@ -228,7 +228,7 @@ in {
 
       # For reference: https://wiki.jenkins.io/display/JENKINS/JenkinsLinuxStartupScript
       script = ''
-        ${pkgs.jdk11}/bin/java ${concatStringsSep " " cfg.extraJavaOptions} -jar ${cfg.package}/webapps/jenkins.war --httpListenAddress=${cfg.listenAddress} \
+        ${pkgs.jdk17}/bin/java ${concatStringsSep " " cfg.extraJavaOptions} -jar ${cfg.package}/webapps/jenkins.war --httpListenAddress=${cfg.listenAddress} \
                                                   --httpPort=${toString cfg.port} \
                                                   --prefix=${cfg.prefix} \
                                                   -Djava.awt.headless=true \
diff --git a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
index 8dc06bf264162..3a1c6c1a371df 100644
--- a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
@@ -30,7 +30,7 @@ in {
       };
 
       accessUser = mkOption {
-        default = "";
+        default = "admin";
         type = types.str;
         description = lib.mdDoc ''
           User id in Jenkins used to reload config.
@@ -48,7 +48,8 @@ in {
       };
 
       accessTokenFile = mkOption {
-        default = "";
+        default = "${config.services.jenkins.home}/secrets/initialAdminPassword";
+        defaultText = literalExpression ''"''${config.services.jenkins.home}/secrets/initialAdminPassword"'';
         type = types.str;
         example = "/run/keys/jenkins-job-builder-access-token";
         description = lib.mdDoc ''
diff --git a/nixos/modules/services/databases/couchdb.nix b/nixos/modules/services/databases/couchdb.nix
index 2a570d09a2c47..16b82b867a3d1 100644
--- a/nixos/modules/services/databases/couchdb.nix
+++ b/nixos/modules/services/databases/couchdb.nix
@@ -34,13 +34,7 @@ in {
 
     services.couchdb = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to run CouchDB Server.
-        '';
-      };
+      enable = mkEnableOption (lib.mdDoc "CouchDB Server");
 
       package = mkOption {
         type = types.package;
diff --git a/nixos/modules/services/databases/influxdb2.nix b/nixos/modules/services/databases/influxdb2.nix
index 92c5a5d7a400d..e74de66ddc2f5 100644
--- a/nixos/modules/services/databases/influxdb2.nix
+++ b/nixos/modules/services/databases/influxdb2.nix
@@ -40,6 +40,7 @@ in
       after = [ "network.target" ];
       environment = {
         INFLUXD_CONFIG_PATH = configFile;
+        ZONEINFO = "${pkgs.tzdata}/share/zoneinfo";
       };
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/influxd --bolt-path \${STATE_DIRECTORY}/influxd.bolt --engine-path \${STATE_DIRECTORY}/engine";
diff --git a/nixos/modules/services/databases/opentsdb.nix b/nixos/modules/services/databases/opentsdb.nix
index 45c84b12a50eb..288b716fce03f 100644
--- a/nixos/modules/services/databases/opentsdb.nix
+++ b/nixos/modules/services/databases/opentsdb.nix
@@ -15,13 +15,7 @@ in {
 
     services.opentsdb = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to run OpenTSDB.
-        '';
-      };
+      enable = mkEnableOption (lib.mdDoc "OpenTSDB");
 
       package = mkOption {
         type = types.package;
@@ -49,7 +43,7 @@ in {
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 4242;
         description = lib.mdDoc ''
           Which port OpenTSDB listens on.
diff --git a/nixos/modules/services/databases/pgmanage.nix b/nixos/modules/services/databases/pgmanage.nix
index 71ce1d8eca477..cbf988d596f46 100644
--- a/nixos/modules/services/databases/pgmanage.nix
+++ b/nixos/modules/services/databases/pgmanage.nix
@@ -85,7 +85,7 @@ in {
     };
 
     port = mkOption {
-      type = types.int;
+      type = types.port;
       default = 8080;
       description = lib.mdDoc ''
         This tells pgmanage what port to listen on for browser requests.
diff --git a/nixos/modules/services/databases/postgresql.xml b/nixos/modules/services/databases/postgresql.xml
index 0ca9f3faed212..e48c578e6ce63 100644
--- a/nixos/modules/services/databases/postgresql.xml
+++ b/nixos/modules/services/databases/postgresql.xml
@@ -72,16 +72,20 @@ Type "help" for help.
 { config, pkgs, ... }:
 {
   <xref linkend="opt-environment.systemPackages" /> = [
-    (pkgs.writeScriptBin "upgrade-pg-cluster" ''
+    (let
+      # XXX specify the postgresql package you'd like to upgrade to.
+      # Do not forget to list the extensions you need.
+      newPostgres = pkgs.postgresql_13.withPackages (pp: [
+        # pp.plv8
+      ]);
+    in pkgs.writeScriptBin "upgrade-pg-cluster" ''
       set -eux
       # XXX it's perhaps advisable to stop all services that depend on postgresql
       systemctl stop postgresql
 
-      # XXX replace `&lt;new version&gt;` with the psqlSchema here
-      export NEWDATA="/var/lib/postgresql/&lt;new version&gt;"
+      export NEWDATA="/var/lib/postgresql/${newPostgres.psqlSchema}"
 
-      # XXX specify the postgresql package you'd like to upgrade to
-      export NEWBIN="${pkgs.postgresql_13}/bin"
+      export NEWBIN="${newPostgres}/bin"
 
       export OLDDATA="${config.<xref linkend="opt-services.postgresql.dataDir"/>}"
       export OLDBIN="${config.<xref linkend="opt-services.postgresql.package"/>}/bin"
@@ -127,12 +131,25 @@ Type "help" for help.
    </listitem>
    <listitem>
     <para>
-     After the upgrade it's advisable to analyze the new cluster (as <literal>su -l postgres</literal> in the
-     <xref linkend="opt-services.postgresql.dataDir" />, in this example <filename>/var/lib/postgresql/13</filename>):
+     After the upgrade it's advisable to analyze the new cluster.
+    </para>
+    <itemizedlist>
+     <listitem>
+      <para>
+       For PostgreSQL ≥ 14, use the <literal>vacuumdb</literal> command printed by the upgrades script.
+      </para>
+     </listitem>
+     <listitem>
+       <para>
+        For PostgreSQL &lt; 14, run (as <literal>su -l postgres</literal> in the <xref linkend="opt-services.postgresql.dataDir" />, in this example <filename>/var/lib/postgresql/13</filename>):
 <programlisting>
 <prompt>$ </prompt>./analyze_new_cluster.sh
 </programlisting>
-     <warning><para>The next step removes the old state-directory!</para></warning>
+       </para>
+     </listitem>
+    </itemizedlist>
+    <para>
+      <warning><para>The next step removes the old state-directory!</para></warning>
 <programlisting>
 <prompt>$ </prompt>./delete_old_cluster.sh
 </programlisting>
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index 319cbbf965c5e..1f143f9c66f6d 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -105,6 +105,13 @@ in {
               '';
             };
 
+            extraParams = mkOption {
+              type = with types; listOf str;
+              default = [];
+              description = lib.mdDoc "Extra parameters to append to redis-server invocation";
+              example = [ "--sentinel" ];
+            };
+
             bind = mkOption {
               type = with types; nullOr str;
               default = "127.0.0.1";
@@ -340,16 +347,24 @@ in {
       after = [ "network.target" ];
 
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/redis-server /run/${redisName name}/redis.conf";
-        ExecStartPre = [("+"+pkgs.writeShellScript "${redisName name}-credentials" (''
-            install -o '${conf.user}' -m 600 ${redisConfig conf.settings} /run/${redisName name}/redis.conf
-          '' + optionalString (conf.requirePassFile != null) ''
-            {
-              printf requirePass' '
-              cat ${escapeShellArg conf.requirePassFile}
-            } >>/run/${redisName name}/redis.conf
-          '')
-        )];
+        ExecStart = "${cfg.package}/bin/redis-server /var/lib/${redisName name}/redis.conf ${escapeShellArgs conf.extraParams}";
+        ExecStartPre = "+"+pkgs.writeShellScript "${redisName name}-prep-conf" (let
+          redisConfVar = "/var/lib/${redisName name}/redis.conf";
+          redisConfRun = "/run/${redisName name}/nixos.conf";
+          redisConfStore = redisConfig conf.settings;
+        in ''
+          touch "${redisConfVar}" "${redisConfRun}"
+          chown '${conf.user}' "${redisConfVar}" "${redisConfRun}"
+          chmod 0600 "${redisConfVar}" "${redisConfRun}"
+          if [ ! -s ${redisConfVar} ]; then
+            echo 'include "${redisConfRun}"' > "${redisConfVar}"
+          fi
+          echo 'include "${redisConfStore}"' > "${redisConfRun}"
+          ${optionalString (conf.requirePassFile != null) ''
+            {echo -n "requirepass "
+            cat ${escapeShellArg conf.requirePassFile}} >> "${redisConfRun}"
+          ''}
+        '');
         Type = "notify";
         # User and group
         User = conf.user;
diff --git a/nixos/modules/services/desktops/geoclue2.nix b/nixos/modules/services/desktops/geoclue2.nix
index a9712b962ffd9..b04f46c26a568 100644
--- a/nixos/modules/services/desktops/geoclue2.nix
+++ b/nixos/modules/services/desktops/geoclue2.nix
@@ -200,6 +200,7 @@ in
     };
 
     systemd.services.geoclue = {
+      after = lib.optionals cfg.enableWifi [ "network-online.target" ];
       # restart geoclue service when the configuration changes
       restartTriggers = [
         config.environment.etc."geoclue/geoclue.conf".source
diff --git a/nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json b/nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json
new file mode 100644
index 0000000000000..689fca88359ba
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json
@@ -0,0 +1,28 @@
+{
+  "context.properties": {
+    "log.level": 0
+  },
+  "context.spa-libs": {
+    "audio.convert.*": "audioconvert/libspa-audioconvert",
+    "support.*": "support/libspa-support"
+  },
+  "context.modules": [
+    {
+      "name": "libpipewire-module-rt",
+      "args": {},
+      "flags": [
+        "ifexists",
+        "nofail"
+      ]
+    },
+    {
+      "name": "libpipewire-module-protocol-native"
+    },
+    {
+      "name": "libpipewire-module-client-node"
+    },
+    {
+      "name": "libpipewire-module-adapter"
+    }
+  ]
+}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json b/nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json
new file mode 100644
index 0000000000000..4f669895d87b6
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json
@@ -0,0 +1,38 @@
+{
+  "context.properties": {},
+  "context.spa-libs": {
+    "audio.convert.*": "audioconvert/libspa-audioconvert",
+    "support.*": "support/libspa-support"
+  },
+  "context.modules": [
+    {
+      "name": "libpipewire-module-rt",
+      "args": {
+        "nice.level": -11
+      },
+      "flags": [
+        "ifexists",
+        "nofail"
+      ]
+    },
+    {
+      "name": "libpipewire-module-protocol-native"
+    },
+    {
+      "name": "libpipewire-module-client-node"
+    },
+    {
+      "name": "libpipewire-module-adapter"
+    },
+    {
+      "name": "libpipewire-module-avb",
+      "args": {}
+    }
+  ],
+  "context.exec": [],
+  "stream.properties": {},
+  "avb.properties": {
+    "ifname": "enp3s0",
+    "vm.overrides": {}
+  }
+}
diff --git a/nixos/modules/services/games/teeworlds.nix b/nixos/modules/services/games/teeworlds.nix
index 083ab3ba94ea7..ffef440330c4e 100644
--- a/nixos/modules/services/games/teeworlds.nix
+++ b/nixos/modules/services/games/teeworlds.nix
@@ -70,7 +70,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8303;
         description = lib.mdDoc ''
           Port the server will listen on.
diff --git a/nixos/modules/services/hardware/pcscd.nix b/nixos/modules/services/hardware/pcscd.nix
index 44d0d3b04a393..a09c64645c485 100644
--- a/nixos/modules/services/hardware/pcscd.nix
+++ b/nixos/modules/services/hardware/pcscd.nix
@@ -5,6 +5,10 @@ with lib;
 let
   cfgFile = pkgs.writeText "reader.conf" config.services.pcscd.readerConfig;
 
+  package = if config.security.polkit.enable
+              then pkgs.pcscliteWithPolkit
+              else pkgs.pcsclite;
+
   pluginEnv = pkgs.buildEnv {
     name = "pcscd-plugins";
     paths = map (p: "${p}/pcsc/drivers") config.services.pcscd.plugins;
@@ -49,8 +53,8 @@ in
 
     environment.etc."reader.conf".source = cfgFile;
 
-    environment.systemPackages = [ pkgs.pcsclite ];
-    systemd.packages = [ (getBin pkgs.pcsclite) ];
+    environment.systemPackages = [ package ];
+    systemd.packages = [ (getBin package) ];
 
     systemd.sockets.pcscd.wantedBy = [ "sockets.target" ];
 
@@ -66,7 +70,7 @@ in
       # 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}" ];
+      serviceConfig.ExecStart = [ "" "${getBin package}/bin/pcscd -f -x -c ${cfgFile}" ];
     };
   };
 }
diff --git a/nixos/modules/services/hardware/sane.nix b/nixos/modules/services/hardware/sane.nix
index 5455cf56e8aa8..dd5c65b1f6a64 100644
--- a/nixos/modules/services/hardware/sane.nix
+++ b/nixos/modules/services/hardware/sane.nix
@@ -28,8 +28,8 @@ let
   };
 
   env = {
-    SANE_CONFIG_DIR = config.hardware.sane.configDir;
-    LD_LIBRARY_PATH = [ "${saneConfig}/lib/sane" ];
+    SANE_CONFIG_DIR = "/etc/sane.d";
+    LD_LIBRARY_PATH = [ "/etc/sane-libs" ];
   };
 
   backends = [ pkg netConf ] ++ optional config.services.saned.enable sanedConf ++ config.hardware.sane.extraBackends;
@@ -70,10 +70,12 @@ in
         Packages providing extra SANE backends to enable.
 
         ::: {.note}
-        The example contains the package for HP scanners.
+        The example contains the package for HP scanners, and the package for
+        Apple AirScan and Microsoft WSD support (supports many
+        vendors/devices).
         :::
       '';
-      example = literalExpression "[ pkgs.hplipWithPlugin ]";
+      example = literalExpression "[ pkgs.hplipWithPlugin pkgs.sane-airscan ]";
     };
 
     hardware.sane.disabledDefaultBackends = mkOption {
@@ -156,6 +158,8 @@ in
 
       environment.systemPackages = backends;
       environment.sessionVariables = env;
+      environment.etc."sane.d".source = config.hardware.sane.configDir;
+      environment.etc."sane-libs".source = "${saneConfig}/lib/sane";
       services.udev.packages = backends;
 
       users.groups.scanner.gid = config.ids.gids.scanner;
diff --git a/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix b/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix
index 9d083a615a2cd..f76ab701c5b9b 100644
--- a/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix
+++ b/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix
@@ -33,7 +33,8 @@ in
 
 stdenv.mkDerivation {
 
-  name = "brscan4-etc-files-0.4.3-3";
+  pname = "brscan4-etc-files";
+  version = "0.4.3-3";
   src = "${brscan4}/opt/brother/scanner/brscan4";
 
   nativeBuildInputs = [ brscan4 ];
diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix
index 4b962da0c037d..7a7f8330243a2 100644
--- a/nixos/modules/services/hardware/udev.nix
+++ b/nixos/modules/services/hardware/udev.nix
@@ -192,7 +192,6 @@ in
   ###### interface
 
   options = {
-
     boot.hardwareScan = mkOption {
       type = types.bool;
       default = true;
@@ -205,6 +204,9 @@ in
     };
 
     services.udev = {
+      enable = mkEnableOption (lib.mdDoc "udev") // {
+        default = true;
+      };
 
       packages = mkOption {
         type = types.listOf types.path;
@@ -345,7 +347,7 @@ in
 
   ###### implementation
 
-  config = mkIf (!config.boot.isContainer) {
+  config = mkIf cfg.enable {
 
     services.udev.extraRules = nixosRules;
 
diff --git a/nixos/modules/services/hardware/udisks2.nix b/nixos/modules/services/hardware/udisks2.nix
index 70667dc6d3b12..7368845dafd55 100644
--- a/nixos/modules/services/hardware/udisks2.nix
+++ b/nixos/modules/services/hardware/udisks2.nix
@@ -62,7 +62,12 @@ in
 
     environment.systemPackages = [ pkgs.udisks2 ];
 
-    environment.etc = mapAttrs' (name: value: nameValuePair "udisks2/${name}" { source = value; } ) configFiles;
+    environment.etc = (mapAttrs' (name: value: nameValuePair "udisks2/${name}" { source = value; } ) configFiles) // {
+      # We need to make sure /etc/libblockdev/conf.d is populated to avoid
+      # warnings
+      "libblockdev/conf.d/00-default.cfg".source = "${pkgs.libblockdev}/etc/libblockdev/conf.d/00-default.cfg";
+      "libblockdev/conf.d/10-lvm-dbus.cfg".source = "${pkgs.libblockdev}/etc/libblockdev/conf.d/10-lvm-dbus.cfg";
+    };
 
     security.polkit.enable = true;
 
diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix
index 26a5e3fe44b81..2962e52c08bba 100644
--- a/nixos/modules/services/home-automation/home-assistant.nix
+++ b/nixos/modules/services/home-automation/home-assistant.nix
@@ -433,6 +433,8 @@ in {
         componentsUsingBluetooth = [
           # Components that require the AF_BLUETOOTH address family
           "august"
+          "august_ble"
+          "airthings_ble"
           "bluemaestro"
           "bluetooth"
           "bluetooth_le_tracker"
@@ -445,15 +447,19 @@ in {
           "govee_ble"
           "homekit_controller"
           "inkbird"
+          "keymitt_ble"
           "led_ble"
           "melnor"
           "moat"
+          "oralb"
           "qingping"
           "sensorpro"
           "sensorpush"
+          "snooz"
           "switchbot"
           "thermobeacon"
           "thermopro"
+          "tilt_ble"
           "xiaomi_ble"
           "yalexs_ble"
         ];
diff --git a/nixos/modules/services/home-automation/zigbee2mqtt.nix b/nixos/modules/services/home-automation/zigbee2mqtt.nix
index 691ca62208e84..71f6e7a258404 100644
--- a/nixos/modules/services/home-automation/zigbee2mqtt.nix
+++ b/nixos/modules/services/home-automation/zigbee2mqtt.nix
@@ -18,7 +18,7 @@ in
   ];
 
   options.services.zigbee2mqtt = {
-    enable = mkEnableOption (lib.mdDoc "enable zigbee2mqtt service");
+    enable = mkEnableOption (lib.mdDoc "zigbee2mqtt service");
 
     package = mkOption {
       description = lib.mdDoc "Zigbee2mqtt package to use";
diff --git a/nixos/modules/services/logging/fluentd.nix b/nixos/modules/services/logging/fluentd.nix
index fe9f4b07e1622..7764aafb2d1af 100644
--- a/nixos/modules/services/logging/fluentd.nix
+++ b/nixos/modules/services/logging/fluentd.nix
@@ -12,11 +12,7 @@ in {
   options = {
 
     services.fluentd = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Whether to enable fluentd.";
-      };
+      enable = mkEnableOption (lib.mdDoc "fluentd");
 
       config = mkOption {
         type = types.lines;
diff --git a/nixos/modules/services/logging/journalwatch.nix b/nixos/modules/services/logging/journalwatch.nix
index a315da3ea0eee..55e2d600ee4fb 100644
--- a/nixos/modules/services/logging/journalwatch.nix
+++ b/nixos/modules/services/logging/journalwatch.nix
@@ -239,7 +239,7 @@ in {
         Type = "oneshot";
         # requires a relative directory name to create beneath /var/lib
         StateDirectory = user;
-        StateDirectoryMode = 0750;
+        StateDirectoryMode = "0750";
         ExecStart = "${pkgs.python3Packages.journalwatch}/bin/journalwatch mail";
         # lowest CPU and IO priority, but both still in best-effort class to prevent starvation
         Nice=19;
diff --git a/nixos/modules/services/logging/logcheck.nix b/nixos/modules/services/logging/logcheck.nix
index b1279f0fe5861..8a277cea6e461 100644
--- a/nixos/modules/services/logging/logcheck.nix
+++ b/nixos/modules/services/logging/logcheck.nix
@@ -109,13 +109,7 @@ in
 {
   options = {
     services.logcheck = {
-      enable = mkOption {
-        default = false;
-        type = types.bool;
-        description = lib.mdDoc ''
-          Enable the logcheck cron job.
-        '';
-      };
+      enable = mkEnableOption (lib.mdDoc "logcheck cron job");
 
       user = mkOption {
         default = "logcheck";
diff --git a/nixos/modules/services/logging/logrotate.nix b/nixos/modules/services/logging/logrotate.nix
index 5ea54a4cf9216..fd41b982678f3 100644
--- a/nixos/modules/services/logging/logrotate.nix
+++ b/nixos/modules/services/logging/logrotate.nix
@@ -5,93 +5,9 @@ with lib;
 let
   cfg = config.services.logrotate;
 
-  # deprecated legacy compat settings
-  # these options will be removed before 22.11 in the following PR:
-  # https://github.com/NixOS/nixpkgs/pull/164169
-  pathOpts = { name, ... }: {
-    options = {
-      enable = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Whether to enable log rotation for this path. This can be used to explicitly disable
-          logging that has been configured by NixOS.
-        '';
-      };
-
-      name = mkOption {
-        type = types.str;
-        internal = true;
-      };
-
-      path = mkOption {
-        type = with types; either str (listOf str);
-        default = name;
-        defaultText = "attribute name";
-        description = lib.mdDoc ''
-          The path to log files to be rotated.
-          Spaces are allowed and normal shell quoting rules apply,
-          with ', ", and \ characters supported.
-        '';
-      };
-
-      user = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          The user account to use for rotation.
-        '';
-      };
-
-      group = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          The group to use for rotation.
-        '';
-      };
-
-      frequency = mkOption {
-        type = types.enum [ "hourly" "daily" "weekly" "monthly" "yearly" ];
-        default = "daily";
-        description = lib.mdDoc ''
-          How often to rotate the logs.
-        '';
-      };
-
-      keep = mkOption {
-        type = types.int;
-        default = 20;
-        description = lib.mdDoc ''
-          How many rotations to keep.
-        '';
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = lib.mdDoc ''
-          Extra logrotate config options for this path. Refer to
-          <https://linux.die.net/man/8/logrotate> for details.
-        '';
-      };
-
-      priority = mkOption {
-        type = types.int;
-        default = 1000;
-        description = lib.mdDoc ''
-          Order of this logrotate block in relation to the others. The semantics are
-          the same as with `lib.mkOrder`. Smaller values have a greater priority.
-        '';
-      };
-    };
-
-    config.name = name;
-  };
-
   generateLine = n: v:
     if builtins.elem n [ "files" "priority" "enable" "global" ] || v == null then null
-    else if builtins.elem n [ "extraConfig" "frequency" ] then "${v}\n"
+    else if builtins.elem n [ "frequency" ] then "${v}\n"
     else if builtins.elem n [ "firstaction" "lastaction" "prerotate" "postrotate" "preremove" ]
          then "${n}\n    ${v}\n  endscript\n"
     else if isInt v then "${n} ${toString v}\n"
@@ -110,25 +26,6 @@ let
         ${generateSection 2 settings}}
     '';
 
-  # below two mapPaths are compat functions
-  mapPathOptToSetting = n: v:
-    if n == "keep" then nameValuePair "rotate" v
-    else if n == "path" then nameValuePair "files" v
-    else nameValuePair n v;
-
-  mapPathsToSettings = path: pathOpts:
-    nameValuePair path (
-      filterAttrs (n: v: ! builtins.elem n [ "user" "group" "name" ] && v != "") (
-        (mapAttrs' mapPathOptToSetting pathOpts) //
-        {
-          su =
-            if pathOpts.user != null
-            then "${pathOpts.user} ${pathOpts.group}"
-            else null;
-        }
-      )
-    );
-
   settings = sortProperties (attrValues (filterAttrs (_: settings: settings.enable) (
     foldAttrs recursiveUpdate { } [
       {
@@ -139,15 +36,7 @@ let
           frequency = "weekly";
           rotate = 4;
         };
-        # compat section
-        extraConfig = {
-          enable = (cfg.extraConfig != "");
-          global = true;
-          extraConfig = cfg.extraConfig;
-          priority = 101;
-        };
       }
-      (mapAttrs' mapPathsToSettings cfg.paths)
       cfg.settings
       { header = { global = true; priority = 100; }; }
     ]
@@ -200,7 +89,9 @@ let
 in
 {
   imports = [
-    (mkRenamedOptionModule [ "services" "logrotate" "config" ] [ "services" "logrotate" "extraConfig" ])
+    (mkRemovedOptionModule [ "services" "logrotate" "config" ] "Modify services.logrotate.settings.header instead")
+    (mkRemovedOptionModule [ "services" "logrotate" "extraConfig" ] "Modify services.logrotate.settings.header instead")
+    (mkRemovedOptionModule [ "services" "logrotate" "paths" ] "Add attributes to services.logrotate.settings instead")
   ];
 
   options = {
@@ -218,6 +109,25 @@ in
           or settings common to all further files settings.
           Refer to <https://linux.die.net/man/8/logrotate> for details.
         '';
+        example = literalExpression ''
+          {
+            # global options
+            header = {
+              dateext = true;
+            };
+            # example custom files
+            "/var/log/mylog.log" = {
+              frequency = "daily";
+              rotate = 3;
+            };
+            "multiple paths" = {
+               files = [
+                "/var/log/first*.log"
+                "/var/log/second.log"
+              ];
+            };
+          };
+          '';
         type = types.attrsOf (types.submodule ({ name, ... }: {
           freeformType = with types; attrsOf (nullOr (oneOf [ int bool str ]));
 
@@ -311,76 +221,10 @@ in
           in this case you can disable the failing check with this option.
         '';
       };
-
-      # deprecated legacy compat settings
-      paths = mkOption {
-        type = with types; attrsOf (submodule pathOpts);
-        default = { };
-        description = lib.mdDoc ''
-          Attribute set of paths to rotate. The order each block appears in the generated configuration file
-          can be controlled by the [priority](#opt-services.logrotate.paths._name_.priority) option
-          using the same semantics as `lib.mkOrder`. Smaller values have a greater priority.
-          This setting has been deprecated in favor of [logrotate settings](#opt-services.logrotate.settings).
-        '';
-        example = literalExpression ''
-          {
-            httpd = {
-              path = "/var/log/httpd/*.log";
-              user = config.services.httpd.user;
-              group = config.services.httpd.group;
-              keep = 7;
-            };
-
-            myapp = {
-              path = "/var/log/myapp/*.log";
-              user = "myuser";
-              group = "mygroup";
-              frequency = "weekly";
-              keep = 5;
-              priority = 1;
-            };
-          }
-        '';
-      };
-
-      extraConfig = mkOption {
-        default = "";
-        type = types.lines;
-        description = lib.mdDoc ''
-          Extra contents to append to the logrotate configuration file. Refer to
-          <https://linux.die.net/man/8/logrotate> for details.
-          This setting has been deprecated in favor of
-          [logrotate settings](#opt-services.logrotate.settings).
-        '';
-      };
     };
   };
 
   config = mkIf cfg.enable {
-    assertions =
-      mapAttrsToList
-        (name: pathOpts:
-          {
-            assertion = (pathOpts.user != null) == (pathOpts.group != null);
-            message = ''
-              If either of `services.logrotate.paths.${name}.user` or `services.logrotate.paths.${name}.group` are specified then *both* must be specified.
-            '';
-          })
-        cfg.paths;
-
-    warnings =
-      (mapAttrsToList
-        (name: pathOpts: ''
-          Using config.services.logrotate.paths.${name} is deprecated and will become unsupported in a future release.
-          Please use services.logrotate.settings instead.
-        '')
-        cfg.paths
-      ) ++
-      (optional (cfg.extraConfig != "") ''
-        Using config.services.logrotate.extraConfig is deprecated and will become unsupported in a future release.
-        Please use services.logrotate.settings with globals=true instead.
-      '');
-
     systemd.services.logrotate = {
       description = "Logrotate Service";
       startAt = "hourly";
diff --git a/nixos/modules/services/mail/listmonk.nix b/nixos/modules/services/mail/listmonk.nix
index 7c298606a5478..c4ea6747196c4 100644
--- a/nixos/modules/services/mail/listmonk.nix
+++ b/nixos/modules/services/mail/listmonk.nix
@@ -202,7 +202,7 @@ in {
         NoNewPrivileges = true;
         CapabilityBoundingSet = "";
         SystemCallArchitecture = "native";
-        SystemCallFilter = [ "@system-service" "~@privileged" "@resources" ];
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
         ProtectDevices = true;
         ProtectControlGroups = true;
         ProtectKernelTunables = true;
diff --git a/nixos/modules/services/mail/mailman.nix b/nixos/modules/services/mail/mailman.nix
index 7ae0a33332e97..198c2f3280f58 100644
--- a/nixos/modules/services/mail/mailman.nix
+++ b/nixos/modules/services/mail/mailman.nix
@@ -263,6 +263,15 @@ in {
 
       serve = {
         enable = mkEnableOption (lib.mdDoc "Automatic nginx and uwsgi setup for mailman-web");
+
+        virtualRoot = mkOption {
+          default = "/";
+          example = lib.literalExpression "/lists";
+          type = types.str;
+          description = lib.mdDoc ''
+            Path to mount the mailman-web django application on.
+          '';
+        };
       };
 
       extraPythonPackages = mkOption {
@@ -433,8 +442,8 @@ in {
       enable = mkDefault true;
       virtualHosts = lib.genAttrs cfg.webHosts (webHost: {
         locations = {
-          "/".extraConfig = "uwsgi_pass unix:/run/mailman-web.socket;";
-          "/static/".alias = webSettings.STATIC_ROOT + "/";
+          ${cfg.serve.virtualRoot}.extraConfig = "uwsgi_pass unix:/run/mailman-web.socket;";
+          "${cfg.serve.virtualRoot}/static/".alias = webSettings.STATIC_ROOT + "/";
         };
       });
     };
@@ -561,7 +570,8 @@ in {
           type = "normal";
           plugins = ["python3"];
           home = webEnv;
-          module = "mailman_web.wsgi";
+          manage-script-name = true;
+          mount = "${cfg.serve.virtualRoot}=mailman_web.wsgi:application";
           http = "127.0.0.1:18507";
         };
         uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
diff --git a/nixos/modules/services/mail/rss2email.nix b/nixos/modules/services/mail/rss2email.nix
index 7b74db1e711b9..54404c5b5f4cb 100644
--- a/nixos/modules/services/mail/rss2email.nix
+++ b/nixos/modules/services/mail/rss2email.nix
@@ -110,7 +110,6 @@ in {
     in
     {
       preStart = ''
-        cp ${conf} /var/rss2email/conf.cfg
         if [ ! -f /var/rss2email/db.json ]; then
           echo '{"version":2,"feeds":[]}' > /var/rss2email/db.json
         fi
@@ -118,7 +117,7 @@ in {
       path = [ pkgs.system-sendmail ];
       serviceConfig = {
         ExecStart =
-          "${pkgs.rss2email}/bin/r2e -c /var/rss2email/conf.cfg -d /var/rss2email/db.json run";
+          "${pkgs.rss2email}/bin/r2e -c ${conf} -d /var/rss2email/db.json run";
         User = "rss2email";
       };
     };
diff --git a/nixos/modules/services/matrix/appservice-discord.nix b/nixos/modules/services/matrix/appservice-discord.nix
index 89b4bc98f494b..15f0f0cc0cdbf 100644
--- a/nixos/modules/services/matrix/appservice-discord.nix
+++ b/nixos/modules/services/matrix/appservice-discord.nix
@@ -137,7 +137,7 @@ in {
         PrivateTmp = true;
         WorkingDirectory = appDir;
         StateDirectory = baseNameOf dataDir;
-        UMask = 0027;
+        UMask = "0027";
         EnvironmentFile = cfg.environmentFile;
 
         ExecStart = ''
diff --git a/nixos/modules/services/matrix/mautrix-facebook.nix b/nixos/modules/services/matrix/mautrix-facebook.nix
index 18c91f649b12d..e74f25df764db 100644
--- a/nixos/modules/services/matrix/mautrix-facebook.nix
+++ b/nixos/modules/services/matrix/mautrix-facebook.nix
@@ -25,6 +25,7 @@ in {
         default = {
           homeserver = {
             address = "http://localhost:8008";
+            software = "standard";
           };
 
           appservice = rec {
diff --git a/nixos/modules/services/matrix/mautrix-telegram.nix b/nixos/modules/services/matrix/mautrix-telegram.nix
index be220e05a5261..fc8b95051ddbe 100644
--- a/nixos/modules/services/matrix/mautrix-telegram.nix
+++ b/nixos/modules/services/matrix/mautrix-telegram.nix
@@ -19,6 +19,10 @@ in {
         apply = recursiveUpdate default;
         inherit (settingsFormat) type;
         default = {
+          homeserver = {
+            software = "standard";
+          };
+
           appservice = rec {
             database = "sqlite:///${dataDir}/mautrix-telegram.db";
             database_opts = {};
@@ -81,7 +85,7 @@ in {
         description = lib.mdDoc ''
           {file}`config.yaml` configuration as a Nix attribute set.
           Configuration options should match those described in
-          [example-config.yaml](https://github.com/tulir/mautrix-telegram/blob/master/example-config.yaml).
+          [example-config.yaml](https://github.com/mautrix/telegram/blob/master/mautrix_telegram/example-config.yaml).
 
           Secret tokens should be specified using {option}`environmentFile`
           instead of this world-readable attribute set.
@@ -124,6 +128,18 @@ in {
       after = [ "network-online.target" ] ++ cfg.serviceDependencies;
       path = [ pkgs.lottieconverter ];
 
+      # mautrix-telegram tries to generate a dotfile in the home directory of
+      # the running user if using a postgresql databse:
+      #
+      #  File "python3.10/site-packages/asyncpg/connect_utils.py", line 257, in _dot_postgre>
+      #    return (pathlib.Path.home() / '.postgresql' / filename).resolve()
+      #  File "python3.10/pathlib.py", line 1000, in home
+      #    return cls("~").expanduser()
+      #  File "python3.10/pathlib.py", line 1440, in expanduser
+      #    raise RuntimeError("Could not determine home directory.")
+      # RuntimeError: Could not determine home directory.
+      environment.HOME = dataDir;
+
       preStart = ''
         # Not all secrets can be passed as environment variable (yet)
         # https://github.com/tulir/mautrix-telegram/issues/584
@@ -162,7 +178,7 @@ in {
         PrivateTmp = true;
         WorkingDirectory = pkgs.mautrix-telegram; # necessary for the database migration scripts to be found
         StateDirectory = baseNameOf dataDir;
-        UMask = 0027;
+        UMask = "0027";
         EnvironmentFile = cfg.environmentFile;
 
         ExecStart = ''
diff --git a/nixos/modules/services/misc/airsonic.nix b/nixos/modules/services/misc/airsonic.nix
index e4448d70a0e46..b8e9dcaf46631 100644
--- a/nixos/modules/services/misc/airsonic.nix
+++ b/nixos/modules/services/misc/airsonic.nix
@@ -48,7 +48,7 @@ in {
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 4040;
         description = lib.mdDoc ''
           The port on which Airsonic will listen for
diff --git a/nixos/modules/services/misc/ankisyncd.nix b/nixos/modules/services/misc/ankisyncd.nix
index 907bd348d7e26..5198b82420230 100644
--- a/nixos/modules/services/misc/ankisyncd.nix
+++ b/nixos/modules/services/misc/ankisyncd.nix
@@ -44,7 +44,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 27701;
         description = lib.mdDoc "ankisyncd port";
       };
diff --git a/nixos/modules/services/misc/apache-kafka.nix b/nixos/modules/services/misc/apache-kafka.nix
index c428cfbc67e04..598907aaf1c61 100644
--- a/nixos/modules/services/misc/apache-kafka.nix
+++ b/nixos/modules/services/misc/apache-kafka.nix
@@ -40,7 +40,7 @@ in {
     port = mkOption {
       description = lib.mdDoc "Port number the broker should listen on.";
       default = 9092;
-      type = types.int;
+      type = types.port;
     };
 
     hostname = mkOption {
diff --git a/nixos/modules/services/misc/ethminer.nix b/nixos/modules/services/misc/ethminer.nix
deleted file mode 100644
index 909c49866e543..0000000000000
--- a/nixos/modules/services/misc/ethminer.nix
+++ /dev/null
@@ -1,117 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.ethminer;
-  poolUrl = escapeShellArg "stratum1+tcp://${cfg.wallet}@${cfg.pool}:${toString cfg.stratumPort}/${cfg.rig}/${cfg.registerMail}";
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.ethminer = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Enable ethminer ether mining.";
-      };
-
-      recheckInterval = mkOption {
-        type = types.ints.unsigned;
-        default = 2000;
-        description = lib.mdDoc "Interval in milliseconds between farm rechecks.";
-      };
-
-      toolkit = mkOption {
-        type = types.enum [ "cuda" "opencl" ];
-        default = "cuda";
-        description = lib.mdDoc "Cuda or opencl toolkit.";
-      };
-
-      apiPort = mkOption {
-        type = types.int;
-        default = -3333;
-        description = lib.mdDoc "Ethminer api port. minus sign puts api in read-only mode.";
-      };
-
-      wallet = mkOption {
-        type = types.str;
-        example = "0x0123456789abcdef0123456789abcdef01234567";
-        description = lib.mdDoc "Ethereum wallet address.";
-      };
-
-      pool = mkOption {
-        type = types.str;
-        example = "eth-us-east1.nanopool.org";
-        description = lib.mdDoc "Mining pool address.";
-      };
-
-      stratumPort = mkOption {
-        type = types.port;
-        default = 9999;
-        description = lib.mdDoc "Stratum protocol tcp port.";
-      };
-
-      rig = mkOption {
-        type = types.str;
-        default = "mining-rig-name";
-        description = lib.mdDoc "Mining rig name.";
-      };
-
-      registerMail = mkOption {
-        type = types.str;
-        example = "email%40example.org";
-        description = lib.mdDoc "Url encoded email address to register with pool.";
-      };
-
-      maxPower = mkOption {
-        type = types.ints.unsigned;
-        default = 113;
-        description = lib.mdDoc "Miner max watt usage.";
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    systemd.services.ethminer = {
-      path = optional (cfg.toolkit == "cuda") [ pkgs.cudaPackages.cudatoolkit ];
-      description = "ethminer ethereum mining service";
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
-
-      serviceConfig = {
-        DynamicUser = true;
-        ExecStartPre = "${pkgs.ethminer}/bin/.ethminer-wrapped --list-devices";
-        ExecStartPost = optional (cfg.toolkit == "cuda") "+${getBin config.boot.kernelPackages.nvidia_x11}/bin/nvidia-smi -pl ${toString cfg.maxPower}";
-        Restart = "always";
-      };
-
-      environment = mkIf (cfg.toolkit == "cuda") {
-        LD_LIBRARY_PATH = "${config.boot.kernelPackages.nvidia_x11}/lib";
-      };
-
-      script = ''
-        ${pkgs.ethminer}/bin/.ethminer-wrapped \
-          --farm-recheck ${toString cfg.recheckInterval} \
-          --report-hashrate \
-          --${cfg.toolkit} \
-          --api-port ${toString cfg.apiPort} \
-          --pool ${poolUrl}
-      '';
-
-    };
-
-  };
-
-}
diff --git a/nixos/modules/services/misc/exhibitor.nix b/nixos/modules/services/misc/exhibitor.nix
index d804b21fd4f92..91a87b55af595 100644
--- a/nixos/modules/services/misc/exhibitor.nix
+++ b/nixos/modules/services/misc/exhibitor.nix
@@ -68,17 +68,12 @@ in
 {
   options = {
     services.exhibitor = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to enable the exhibitor server.
-        '';
-      };
+      enable = mkEnableOption (lib.mdDoc "exhibitor server");
+
       # See https://github.com/soabase/exhibitor/wiki/Running-Exhibitor for what these mean
       # General options for any type of config
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8080;
         description = lib.mdDoc ''
           The port for exhibitor to listen on and communicate with other exhibitors.
diff --git a/nixos/modules/services/misc/geoipupdate.nix b/nixos/modules/services/misc/geoipupdate.nix
index fafe4e3f24197..27c1157e9a8c7 100644
--- a/nixos/modules/services/misc/geoipupdate.nix
+++ b/nixos/modules/services/misc/geoipupdate.nix
@@ -183,7 +183,7 @@ in
         DynamicUser = true;
         ReadWritePaths = cfg.settings.DatabaseDirectory;
         RuntimeDirectory = "geoipupdate";
-        RuntimeDirectoryMode = 0700;
+        RuntimeDirectoryMode = "0700";
         CapabilityBoundingSet = "";
         PrivateDevices = true;
         PrivateMounts = true;
@@ -197,7 +197,7 @@ in
         ProtectKernelTunables = true;
         ProtectProc = "invisible";
         ProcSubset = "pid";
-        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
         RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
         RestrictRealtime = true;
         RestrictNamespaces = true;
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index d9dece3343f6d..ac598108a01ef 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -592,7 +592,7 @@ in
         PrivateMounts = true;
         # System Call Filtering
         SystemCallArchitectures = "native";
-        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @resources @setuid @swap";
+        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @setuid @swap";
       };
 
       environment = {
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index 4988517a9b66a..13453d9cc7854 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -260,6 +260,7 @@ in {
     (mkRenamedOptionModule [ "services" "gitlab" "stateDir" ] [ "services" "gitlab" "statePath" ])
     (mkRenamedOptionModule [ "services" "gitlab" "backupPath" ] [ "services" "gitlab" "backup" "path" ])
     (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "")
+    (mkRemovedOptionModule [ "services" "gitlab" "logrotate" "extraConfig" ] "Modify services.logrotate.settings.gitlab directly instead")
   ];
 
   options = {
@@ -871,15 +872,6 @@ in {
           default = 30;
           description = lib.mdDoc "How many rotations to keep.";
         };
-
-        extraConfig = mkOption {
-          type = types.lines;
-          default = "";
-          description = lib.mdDoc ''
-            Extra logrotate config options for this path. Refer to
-            <https://linux.die.net/man/8/logrotate> for details.
-          '';
-        };
       };
 
       workhorse.config = mkOption {
@@ -1042,7 +1034,6 @@ in {
           rotate = cfg.logrotate.keep;
           copytruncate = true;
           compress = true;
-          extraConfig = cfg.logrotate.extraConfig;
         };
       };
     };
diff --git a/nixos/modules/services/misc/gogs.nix b/nixos/modules/services/misc/gogs.nix
index e726f2c5c7ce0..fa172ed277dc4 100644
--- a/nixos/modules/services/misc/gogs.nix
+++ b/nixos/modules/services/misc/gogs.nix
@@ -90,7 +90,7 @@ in
         };
 
         port = mkOption {
-          type = types.int;
+          type = types.port;
           default = 3306;
           description = lib.mdDoc "Database host port.";
         };
@@ -167,7 +167,7 @@ in
       };
 
       httpPort = mkOption {
-        type = types.int;
+        type = types.port;
         default = 3000;
         description = lib.mdDoc "HTTP listen port.";
       };
diff --git a/nixos/modules/services/misc/gollum.nix b/nixos/modules/services/misc/gollum.nix
index ca6f42736a10e..4eec9610b5e9f 100644
--- a/nixos/modules/services/misc/gollum.nix
+++ b/nixos/modules/services/misc/gollum.nix
@@ -8,11 +8,7 @@ in
 
 {
   options.services.gollum = {
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc "Enable the Gollum service.";
-    };
+    enable = mkEnableOption (lib.mdDoc "Gollum service");
 
     address = mkOption {
       type = types.str;
@@ -21,7 +17,7 @@ in
     };
 
     port = mkOption {
-      type = types.int;
+      type = types.port;
       default = 4567;
       description = lib.mdDoc "Port on which the web server will run.";
     };
diff --git a/nixos/modules/services/misc/languagetool.nix b/nixos/modules/services/misc/languagetool.nix
index 20dc9b6b447dd..9adf792373b5a 100644
--- a/nixos/modules/services/misc/languagetool.nix
+++ b/nixos/modules/services/misc/languagetool.nix
@@ -70,7 +70,7 @@ in {
             --port ${toString cfg.port} \
             ${optionalString cfg.public "--public"} \
             ${optionalString (cfg.allowOrigin != null) "--allow-origin ${cfg.allowOrigin}"} \
-            "--configuration" ${settingsFormat.generate "languagetool.conf" cfg.settings}
+            "--config" ${settingsFormat.generate "languagetool.conf" cfg.settings}
           '';
       };
     };
diff --git a/nixos/modules/services/misc/mx-puppet-discord.nix b/nixos/modules/services/misc/mx-puppet-discord.nix
index 33a6c8f26a957..36c9f8b122ea2 100644
--- a/nixos/modules/services/misc/mx-puppet-discord.nix
+++ b/nixos/modules/services/misc/mx-puppet-discord.nix
@@ -107,7 +107,7 @@ in {
         PrivateTmp = true;
         WorkingDirectory = pkgs.mx-puppet-discord;
         StateDirectory = baseNameOf dataDir;
-        UMask = 0027;
+        UMask = "0027";
 
         ExecStart = ''
           ${pkgs.mx-puppet-discord}/bin/mx-puppet-discord \
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index ba3ea4c47ac1d..26e7cbfca733f 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -59,7 +59,7 @@ let
         ${mkKeyValuePairs cfg.settings}
         ${cfg.extraOptions}
       '';
-      checkPhase =
+      checkPhase = lib.optionalString cfg.checkConfig (
         if pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform then ''
           echo "Ignoring validation for cross-compilation"
         ''
@@ -72,9 +72,9 @@ let
             ${cfg.package}/bin/nix show-config ${optionalString (isNixAtLeast "2.3pre") "--no-net"} \
               ${optionalString (isNixAtLeast "2.4pre") "--option experimental-features nix-command"} \
             |& sed -e 's/^warning:/error:/' \
-            | (! grep '${if cfg.checkConfig then "^error:" else "^error: unknown setting"}')
+            | (! grep '${if cfg.checkAllErrors then "^error:" else "^error: unknown setting"}')
           set -o pipefail
-        '';
+        '');
     };
 
   legacyConfMappings = {
@@ -206,7 +206,7 @@ in
 
       daemonIOSchedPriority = mkOption {
         type = types.int;
-        default = 0;
+        default = 4;
         example = 1;
         description = lib.mdDoc ''
           Nix daemon process I/O scheduling priority. This priority propagates
@@ -395,8 +395,15 @@ in
         type = types.bool;
         default = true;
         description = lib.mdDoc ''
-          If enabled (the default), checks for data type mismatches and that Nix
-          can parse the generated nix.conf.
+          If enabled, checks that Nix can parse the generated nix.conf.
+        '';
+      };
+
+      checkAllErrors = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc ''
+          If enabled, checks the nix.conf parsing for any kind of error. When disabled, checks only for unknown settings.
         '';
       };
 
diff --git a/nixos/modules/services/misc/ntfy-sh.nix b/nixos/modules/services/misc/ntfy-sh.nix
new file mode 100644
index 0000000000000..9d52fcf25364f
--- /dev/null
+++ b/nixos/modules/services/misc/ntfy-sh.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ntfy-sh;
+
+  settingsFormat = pkgs.formats.yaml { };
+in
+
+{
+  options.services.ntfy-sh = {
+    enable = mkEnableOption (mdDoc "[ntfy-sh](https://ntfy.sh), a push notification service");
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.ntfy-sh;
+      defaultText = literalExpression "pkgs.ntfy-sh";
+      description = mdDoc "The ntfy.sh package to use.";
+    };
+
+    user = mkOption {
+      default = "ntfy-sh";
+      type = types.str;
+      description = lib.mdDoc "User the ntfy-sh server runs under.";
+    };
+
+    group = mkOption {
+      default = "ntfy-sh";
+      type = types.str;
+      description = lib.mdDoc "Primary group of ntfy-sh user.";
+    };
+
+    settings = mkOption {
+      type = types.submodule { freeformType = settingsFormat.type; };
+
+      default = { };
+
+      example = literalExpression ''
+        {
+          listen-http = ":8080";
+        }
+      '';
+
+      description = mdDoc ''
+        Configuration for ntfy.sh, supported values are [here](https://ntfy.sh/docs/config/#config-options).
+      '';
+    };
+  };
+
+  config =
+    let
+      configuration = settingsFormat.generate "server.yml" cfg.settings;
+    in
+    mkIf cfg.enable {
+      # to configure access control via the cli
+      environment = {
+        etc."ntfy/server.yml".source = configuration;
+        systemPackages = [ cfg.package ];
+      };
+
+      systemd.services.ntfy-sh = {
+        description = "Push notifications server";
+
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+
+        serviceConfig = {
+          ExecStart = "${cfg.package}/bin/ntfy serve -c ${configuration}";
+          User = cfg.user;
+
+          AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+          PrivateTmp = true;
+          NoNewPrivileges = true;
+          CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+          ProtectSystem = "full";
+          ProtectKernelTunables = true;
+          ProtectKernelModules = true;
+          ProtectKernelLogs = true;
+          ProtectControlGroups = true;
+          PrivateDevices = true;
+          RestrictSUIDSGID = true;
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          MemoryDenyWriteExecute = true;
+        };
+      };
+
+      users.groups = optionalAttrs (cfg.group == "ntfy-sh") {
+        ntfy-sh = { };
+      };
+
+      users.users = optionalAttrs (cfg.user == "ntfy-sh") {
+        ntfy-sh = {
+          isSystemUser = true;
+          group = cfg.group;
+        };
+      };
+    };
+}
diff --git a/nixos/modules/services/misc/parsoid.nix b/nixos/modules/services/misc/parsoid.nix
index 101ece5ab4c63..6f4a340c8a184 100644
--- a/nixos/modules/services/misc/parsoid.nix
+++ b/nixos/modules/services/misc/parsoid.nix
@@ -70,7 +70,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8000;
         description = lib.mdDoc ''
           Port to listen on.
diff --git a/nixos/modules/services/misc/podgrab.nix b/nixos/modules/services/misc/podgrab.nix
index 10c7bc96b8f04..c0a1247185050 100644
--- a/nixos/modules/services/misc/podgrab.nix
+++ b/nixos/modules/services/misc/podgrab.nix
@@ -36,7 +36,7 @@ in
       };
       serviceConfig = {
         DynamicUser = true;
-        EnvironmentFile = lib.optional (cfg.passwordFile != null) [
+        EnvironmentFile = lib.optionals (cfg.passwordFile != null) [
           cfg.passwordFile
         ];
         ExecStart = "${pkgs.podgrab}/bin/podgrab";
diff --git a/nixos/modules/services/misc/portunus.nix b/nixos/modules/services/misc/portunus.nix
index 9e34b25e45273..0b283ea27d82c 100644
--- a/nixos/modules/services/misc/portunus.nix
+++ b/nixos/modules/services/misc/portunus.nix
@@ -212,9 +212,9 @@ in
 
         staticClients = forEach cfg.dex.oidcClients (client: {
           inherit (client) id;
-          redirectURIs = [ client.callbackURI ];
+          redirectURIs = [ client.callbackURL ];
           name = "OIDC for ${client.id}";
-          secret = "$DEX_CLIENT_${client.id}";
+          secretEnv = "DEX_CLIENT_${client.id}";
         });
       };
     };
diff --git a/nixos/modules/services/misc/pykms.nix b/nixos/modules/services/misc/pykms.nix
index d24cd1bfa05b6..314388e0152ef 100644
--- a/nixos/modules/services/misc/pykms.nix
+++ b/nixos/modules/services/misc/pykms.nix
@@ -28,7 +28,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 1688;
         description = lib.mdDoc "The port on which to listen.";
       };
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 5fd7e79230eaa..75c5a4e26e0f6 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -161,7 +161,7 @@ in
         };
 
         port = mkOption {
-          type = types.int;
+          type = types.port;
           default = if cfg.database.type == "postgresql" then 5432 else 3306;
           defaultText = literalExpression "3306";
           description = lib.mdDoc "Database host port.";
diff --git a/nixos/modules/services/misc/rippled.nix b/nixos/modules/services/misc/rippled.nix
index a3ba3dd2c37d3..d14b6421b7424 100644
--- a/nixos/modules/services/misc/rippled.nix
+++ b/nixos/modules/services/misc/rippled.nix
@@ -98,7 +98,7 @@ let
 
       port = mkOption {
         description = lib.mdDoc "Port where rippled listens.";
-        type = types.int;
+        type = types.port;
       };
 
       protocol = mkOption {
diff --git a/nixos/modules/services/misc/rmfakecloud.nix b/nixos/modules/services/misc/rmfakecloud.nix
index 25857c173b6ff..1cdfdeceabcde 100644
--- a/nixos/modules/services/misc/rmfakecloud.nix
+++ b/nixos/modules/services/misc/rmfakecloud.nix
@@ -138,7 +138,7 @@ in {
         SystemCallArchitectures = "native";
         WorkingDirectory = serviceDataDir;
         StateDirectory = baseNameOf serviceDataDir;
-        UMask = 0027;
+        UMask = "0027";
       };
     };
   };
diff --git a/nixos/modules/services/misc/sonarr.nix b/nixos/modules/services/misc/sonarr.nix
index 5a5c9b5aaad86..65c51d9677d9e 100644
--- a/nixos/modules/services/misc/sonarr.nix
+++ b/nixos/modules/services/misc/sonarr.nix
@@ -35,6 +35,15 @@ in
         default = "sonarr";
         description = lib.mdDoc "Group under which Sonaar runs.";
       };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.sonarr;
+        defaultText = literalExpression "pkgs.sonarr";
+        description = lib.mdDoc ''
+          Sonarr package to use.
+        '';
+      };
     };
   };
 
@@ -52,7 +61,7 @@ in
         Type = "simple";
         User = cfg.user;
         Group = cfg.group;
-        ExecStart = "${pkgs.sonarr}/bin/NzbDrone -nobrowser -data='${cfg.dataDir}'";
+        ExecStart = "${cfg.package}/bin/NzbDrone -nobrowser -data='${cfg.dataDir}'";
         Restart = "on-failure";
       };
     };
diff --git a/nixos/modules/services/misc/sourcehut/default.nix b/nixos/modules/services/misc/sourcehut/default.nix
index 9177f3cd022eb..a79149c8f58ad 100644
--- a/nixos/modules/services/misc/sourcehut/default.nix
+++ b/nixos/modules/services/misc/sourcehut/default.nix
@@ -1419,5 +1419,5 @@ in
   ];
 
   meta.doc = ./sourcehut.xml;
-  meta.maintainers = with maintainers; [ julm tomberek ];
+  meta.maintainers = with maintainers; [ tomberek ];
 }
diff --git a/nixos/modules/services/misc/synergy.nix b/nixos/modules/services/misc/synergy.nix
index e630992f797bf..0cbdc7599c0f8 100644
--- a/nixos/modules/services/misc/synergy.nix
+++ b/nixos/modules/services/misc/synergy.nix
@@ -115,7 +115,7 @@ in
         description = "Synergy server";
         wantedBy = optional cfgS.autoStart "graphical-session.target";
         path = [ pkgs.synergy ];
-        serviceConfig.ExecStart = ''${pkgs.synergy}/bin/synergys -c ${cfgS.configFile} -f${optionalString (cfgS.address != "") " -a ${cfgS.address}"}${optionalString (cfgS.screenName != "") " -n ${cfgS.screenName}"}${optionalString cfgS.tls.enable " --enable-crypto"}${optionalString (cfgS.tls.cert != null) (" --tls-cert=${cfgS.tls.cert}")}'';
+        serviceConfig.ExecStart = ''${pkgs.synergy}/bin/synergys -c ${cfgS.configFile} -f${optionalString (cfgS.address != "") " -a ${cfgS.address}"}${optionalString (cfgS.screenName != "") " -n ${cfgS.screenName}"}${optionalString cfgS.tls.enable " --enable-crypto"}${optionalString (cfgS.tls.cert != null) (" --tls-cert ${cfgS.tls.cert}")}'';
         serviceConfig.Restart = "on-failure";
       };
     })
diff --git a/nixos/modules/services/misc/tautulli.nix b/nixos/modules/services/misc/tautulli.nix
index 0efd0839bc191..b29e9dc0c8d5f 100644
--- a/nixos/modules/services/misc/tautulli.nix
+++ b/nixos/modules/services/misc/tautulli.nix
@@ -27,7 +27,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8181;
         description = lib.mdDoc "TCP port where Tautulli listens.";
       };
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
index e031fab5970b8..109415a20ee63 100644
--- a/nixos/modules/services/misc/zoneminder.nix
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -97,7 +97,7 @@ in {
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8095;
         description = lib.mdDoc ''
           The port on which to listen.
diff --git a/nixos/modules/services/misc/zookeeper.nix b/nixos/modules/services/misc/zookeeper.nix
index 17d4a00f28f1e..fb51be698e72a 100644
--- a/nixos/modules/services/misc/zookeeper.nix
+++ b/nixos/modules/services/misc/zookeeper.nix
@@ -24,16 +24,12 @@ let
 in {
 
   options.services.zookeeper = {
-    enable = mkOption {
-      description = lib.mdDoc "Whether to enable Zookeeper.";
-      default = false;
-      type = types.bool;
-    };
+    enable = mkEnableOption (lib.mdDoc "Zookeeper");
 
     port = mkOption {
       description = lib.mdDoc "Zookeeper Client port.";
       default = 2181;
-      type = types.int;
+      type = types.port;
     };
 
     id = mkOption {
diff --git a/nixos/modules/services/monitoring/alerta.nix b/nixos/modules/services/monitoring/alerta.nix
index cf94f9813e866..6c7ebec4191c4 100644
--- a/nixos/modules/services/monitoring/alerta.nix
+++ b/nixos/modules/services/monitoring/alerta.nix
@@ -24,7 +24,7 @@ in
     enable = mkEnableOption (lib.mdDoc "alerta");
 
     port = mkOption {
-      type = types.int;
+      type = types.port;
       default = 5000;
       description = lib.mdDoc "Port of Alerta";
     };
diff --git a/nixos/modules/services/monitoring/arbtt.nix b/nixos/modules/services/monitoring/arbtt.nix
index 8bf4f78cc7279..f07ecc5d5dd04 100644
--- a/nixos/modules/services/monitoring/arbtt.nix
+++ b/nixos/modules/services/monitoring/arbtt.nix
@@ -7,13 +7,7 @@ let
 in {
   options = {
     services.arbtt = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Enable the arbtt statistics capture service.
-        '';
-      };
+      enable = mkEnableOption (lib.mdDoc "Arbtt statistics capture service");
 
       package = mkOption {
         type = types.package;
diff --git a/nixos/modules/services/monitoring/bosun.nix b/nixos/modules/services/monitoring/bosun.nix
index 27966e089eb9e..dc75fda6ed8aa 100644
--- a/nixos/modules/services/monitoring/bosun.nix
+++ b/nixos/modules/services/monitoring/bosun.nix
@@ -22,13 +22,7 @@ in {
 
     services.bosun = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to run bosun.
-        '';
-      };
+      enable = mkEnableOption (lib.mdDoc "bosun");
 
       package = mkOption {
         type = types.package;
diff --git a/nixos/modules/services/monitoring/cadvisor.nix b/nixos/modules/services/monitoring/cadvisor.nix
index d5e4403101621..a8fba4e6e8cef 100644
--- a/nixos/modules/services/monitoring/cadvisor.nix
+++ b/nixos/modules/services/monitoring/cadvisor.nix
@@ -8,11 +8,7 @@ let
 in {
   options = {
     services.cadvisor = {
-      enable = mkOption {
-        default = false;
-        type = types.bool;
-        description = lib.mdDoc "Whether to enable cadvisor service.";
-      };
+      enable = mkEnableOption (lib.mdDoc "Cadvisor service");
 
       listenAddress = mkOption {
         default = "127.0.0.1";
@@ -22,7 +18,7 @@ in {
 
       port = mkOption {
         default = 8080;
-        type = types.int;
+        type = types.port;
         description = lib.mdDoc "Cadvisor listening port";
       };
 
diff --git a/nixos/modules/services/monitoring/datadog-agent.nix b/nixos/modules/services/monitoring/datadog-agent.nix
index a7f6fa23aa65d..15deef18b60f3 100644
--- a/nixos/modules/services/monitoring/datadog-agent.nix
+++ b/nixos/modules/services/monitoring/datadog-agent.nix
@@ -49,13 +49,7 @@ let
   };
 in {
   options.services.datadog-agent = {
-    enable = mkOption {
-      description = lib.mdDoc ''
-        Whether to enable the datadog-agent v7 monitoring service
-      '';
-      default = false;
-      type = types.bool;
-    };
+    enable = mkEnableOption (lib.mdDoc "Datadog-agent v7 monitoring service");
 
     package = mkOption {
       default = pkgs.datadog-agent;
diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix
index 549da138fe236..60f6e84c63c7d 100644
--- a/nixos/modules/services/monitoring/grafana-image-renderer.nix
+++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix
@@ -106,9 +106,9 @@ in {
       }
     ];
 
-    services.grafana.extraOptions = mkIf cfg.provisionGrafana {
-      RENDERING_SERVER_URL = "http://localhost:${toString cfg.settings.service.port}/render";
-      RENDERING_CALLBACK_URL = "http://localhost:${toString config.services.grafana.port}";
+    services.grafana.settings.rendering = mkIf cfg.provisionGrafana {
+      url = "http://localhost:${toString cfg.settings.service.port}/render";
+      callback_url = "http://localhost:${toString config.services.grafana.port}";
     };
 
     services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
diff --git a/nixos/modules/services/monitoring/grafana-reporter.nix b/nixos/modules/services/monitoring/grafana-reporter.nix
index add725e7ba2a4..eac304d63aa13 100644
--- a/nixos/modules/services/monitoring/grafana-reporter.nix
+++ b/nixos/modules/services/monitoring/grafana-reporter.nix
@@ -23,7 +23,7 @@ in {
       port = mkOption {
         description = lib.mdDoc "Grafana port.";
         default = 3000;
-        type = types.int;
+        type = types.port;
       };
 
     };
@@ -36,7 +36,7 @@ in {
     port = mkOption {
       description = lib.mdDoc "Listening port.";
       default = 8686;
-      type = types.int;
+      type = types.port;
     };
 
     templateDir = mkOption {
diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix
index 15ca11a2e1642..52d5cab9f5159 100644
--- a/nixos/modules/services/monitoring/grafana.nix
+++ b/nixos/modules/services/monitoring/grafana.nix
@@ -5,86 +5,29 @@ with lib;
 let
   cfg = config.services.grafana;
   opt = options.services.grafana;
+  provisioningSettingsFormat = pkgs.formats.yaml {};
   declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins);
-  useMysql = cfg.database.type == "mysql";
-  usePostgresql = cfg.database.type == "postgres";
-
-  envOptions = {
-    PATHS_DATA = cfg.dataDir;
-    PATHS_PLUGINS = if builtins.isNull cfg.declarativePlugins then "${cfg.dataDir}/plugins" else declarativePlugins;
-    PATHS_LOGS = "${cfg.dataDir}/log";
-
-    SERVER_SERVE_FROM_SUBPATH = boolToString cfg.server.serveFromSubPath;
-    SERVER_PROTOCOL = cfg.protocol;
-    SERVER_HTTP_ADDR = cfg.addr;
-    SERVER_HTTP_PORT = cfg.port;
-    SERVER_SOCKET = cfg.socket;
-    SERVER_DOMAIN = cfg.domain;
-    SERVER_ROOT_URL = cfg.rootUrl;
-    SERVER_STATIC_ROOT_PATH = cfg.staticRootPath;
-    SERVER_CERT_FILE = cfg.certFile;
-    SERVER_CERT_KEY = cfg.certKey;
-
-    DATABASE_TYPE = cfg.database.type;
-    DATABASE_HOST = cfg.database.host;
-    DATABASE_NAME = cfg.database.name;
-    DATABASE_USER = cfg.database.user;
-    DATABASE_PASSWORD = cfg.database.password;
-    DATABASE_PATH = cfg.database.path;
-    DATABASE_CONN_MAX_LIFETIME = cfg.database.connMaxLifetime;
-
-    SECURITY_ADMIN_USER = cfg.security.adminUser;
-    SECURITY_ADMIN_PASSWORD = cfg.security.adminPassword;
-    SECURITY_SECRET_KEY = cfg.security.secretKey;
-
-    USERS_ALLOW_SIGN_UP = boolToString cfg.users.allowSignUp;
-    USERS_ALLOW_ORG_CREATE = boolToString cfg.users.allowOrgCreate;
-    USERS_AUTO_ASSIGN_ORG = boolToString cfg.users.autoAssignOrg;
-    USERS_AUTO_ASSIGN_ORG_ROLE = cfg.users.autoAssignOrgRole;
-
-    AUTH_DISABLE_LOGIN_FORM = boolToString cfg.auth.disableLoginForm;
-
-    AUTH_ANONYMOUS_ENABLED = boolToString cfg.auth.anonymous.enable;
-    AUTH_ANONYMOUS_ORG_NAME = cfg.auth.anonymous.org_name;
-    AUTH_ANONYMOUS_ORG_ROLE = cfg.auth.anonymous.org_role;
-
-    AUTH_AZUREAD_NAME = "Azure AD";
-    AUTH_AZUREAD_ENABLED = boolToString cfg.auth.azuread.enable;
-    AUTH_AZUREAD_ALLOW_SIGN_UP = boolToString cfg.auth.azuread.allowSignUp;
-    AUTH_AZUREAD_CLIENT_ID = cfg.auth.azuread.clientId;
-    AUTH_AZUREAD_SCOPES = "openid email profile";
-    AUTH_AZUREAD_AUTH_URL = "https://login.microsoftonline.com/${cfg.auth.azuread.tenantId}/oauth2/v2.0/authorize";
-    AUTH_AZUREAD_TOKEN_URL = "https://login.microsoftonline.com/${cfg.auth.azuread.tenantId}/oauth2/v2.0/token";
-    AUTH_AZUREAD_ALLOWED_DOMAINS = cfg.auth.azuread.allowedDomains;
-    AUTH_AZUREAD_ALLOWED_GROUPS = cfg.auth.azuread.allowedGroups;
-    AUTH_AZUREAD_ROLE_ATTRIBUTE_STRICT = false;
-
-    AUTH_GOOGLE_ENABLED = boolToString cfg.auth.google.enable;
-    AUTH_GOOGLE_ALLOW_SIGN_UP = boolToString cfg.auth.google.allowSignUp;
-    AUTH_GOOGLE_CLIENT_ID = cfg.auth.google.clientId;
-
-    ANALYTICS_REPORTING_ENABLED = boolToString cfg.analytics.reporting.enable;
-
-    SMTP_ENABLED = boolToString cfg.smtp.enable;
-    SMTP_HOST = cfg.smtp.host;
-    SMTP_USER = cfg.smtp.user;
-    SMTP_PASSWORD = cfg.smtp.password;
-    SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
-  } // cfg.extraOptions;
+  useMysql = cfg.settings.database.type == "mysql";
+  usePostgresql = cfg.settings.database.type == "postgres";
+
+  settingsFormatIni = pkgs.formats.ini {};
+  configFile = settingsFormatIni.generate "config.ini" cfg.settings;
 
   datasourceConfiguration = {
     apiVersion = 1;
     datasources = cfg.provision.datasources;
   };
 
-  datasourceFile = pkgs.writeText "datasource.yaml" (builtins.toJSON datasourceConfiguration);
+  datasourceFileNew = if (cfg.provision.datasources.path == null) then provisioningSettingsFormat.generate "datasource.yaml" cfg.provision.datasources.settings else cfg.provision.datasources.path;
+  datasourceFile = if (builtins.isList cfg.provision.datasources) then provisioningSettingsFormat.generate "datasource.yaml" datasourceConfiguration else datasourceFileNew;
 
   dashboardConfiguration = {
     apiVersion = 1;
     providers = cfg.provision.dashboards;
   };
 
-  dashboardFile = pkgs.writeText "dashboard.yaml" (builtins.toJSON dashboardConfiguration);
+  dashboardFileNew = if (cfg.provision.dashboards.path == null) then provisioningSettingsFormat.generate "dashboard.yaml" cfg.provision.dashboards.settings else cfg.provision.dashboards.path;
+  dashboardFile = if (builtins.isList cfg.provision.dashboards) then provisioningSettingsFormat.generate "dashboard.yaml" dashboardConfiguration else dashboardFileNew;
 
   notifierConfiguration = {
     apiVersion = 1;
@@ -93,11 +36,25 @@ let
 
   notifierFile = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration);
 
+  generateAlertingProvisioningYaml = x: if (cfg.provision.alerting."${x}".path == null)
+                                        then provisioningSettingsFormat.generate "${x}.yaml" cfg.provision.alerting."${x}".settings
+                                        else cfg.provision.alerting."${x}".path;
+  rulesFile = generateAlertingProvisioningYaml "rules";
+  contactPointsFile = generateAlertingProvisioningYaml "contactPoints";
+  policiesFile = generateAlertingProvisioningYaml "policies";
+  templatesFile = generateAlertingProvisioningYaml "templates";
+  muteTimingsFile = generateAlertingProvisioningYaml "muteTimings";
+
   provisionConfDir =  pkgs.runCommand "grafana-provisioning" { } ''
-    mkdir -p $out/{datasources,dashboards,notifiers}
+    mkdir -p $out/{datasources,dashboards,notifiers,alerting}
     ln -sf ${datasourceFile} $out/datasources/datasource.yaml
     ln -sf ${dashboardFile} $out/dashboards/dashboard.yaml
     ln -sf ${notifierFile} $out/notifiers/notifier.yaml
+    ln -sf ${rulesFile} $out/alerting/rules.yaml
+    ln -sf ${contactPointsFile} $out/alerting/contactPoints.yaml
+    ln -sf ${policiesFile} $out/alerting/policies.yaml
+    ln -sf ${templatesFile} $out/alerting/templates.yaml
+    ln -sf ${muteTimingsFile} $out/alerting/muteTimings.yaml
   '';
 
   # Get a submodule without any embedded metadata:
@@ -105,6 +62,8 @@ let
 
   # http://docs.grafana.org/administration/provisioning/#datasources
   grafanaTypes.datasourceConfig = types.submodule {
+    freeformType = provisioningSettingsFormat.type;
+
     options = {
       name = mkOption {
         type = types.str;
@@ -119,11 +78,6 @@ let
         default = "proxy";
         description = lib.mdDoc "Access mode. proxy or direct (Server or Browser in the UI). Required.";
       };
-      orgId = mkOption {
-        type = types.int;
-        default = 1;
-        description = lib.mdDoc "Org id. will default to orgId 1 if not specified.";
-      };
       uid = mkOption {
         type = types.nullOr types.str;
         default = null;
@@ -131,114 +85,68 @@ let
       };
       url = mkOption {
         type = types.str;
+        default = "localhost";
         description = lib.mdDoc "Url of the datasource.";
       };
-      password = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = lib.mdDoc "Database password, if used.";
-      };
-      user = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = lib.mdDoc "Database user, if used.";
-      };
-      database = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = lib.mdDoc "Database name, if used.";
-      };
-      basicAuth = mkOption {
-        type = types.nullOr types.bool;
-        default = null;
-        description = lib.mdDoc "Enable/disable basic auth.";
+      editable = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc "Allow users to edit datasources from the UI.";
       };
-      basicAuthUser = mkOption {
+      password = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = lib.mdDoc "Basic auth username.";
+        description = lib.mdDoc ''
+          Database password, if used. Please note that the contents of this option
+          will end up in a world-readable Nix store. Use the file provider
+          pointing at a reasonably secured file in the local filesystem
+          to work around that. Look at the documentation for details:
+          <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+        '';
       };
       basicAuthPassword = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = lib.mdDoc "Basic auth password.";
-      };
-      withCredentials = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Enable/disable with credentials headers.";
-      };
-      isDefault = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Mark as default datasource. Max one per org.";
-      };
-      jsonData = mkOption {
-        type = types.nullOr types.attrs;
-        default = null;
-        description = lib.mdDoc "Datasource specific configuration.";
+        description = lib.mdDoc ''
+          Basic auth password. Please note that the contents of this option
+          will end up in a world-readable Nix store. Use the file provider
+          pointing at a reasonably secured file in the local filesystem
+          to work around that. Look at the documentation for details:
+          <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+        '';
       };
       secureJsonData = mkOption {
         type = types.nullOr types.attrs;
         default = null;
-        description = lib.mdDoc "Datasource specific secure configuration.";
-      };
-      version = mkOption {
-        type = types.int;
-        default = 1;
-        description = lib.mdDoc "Version.";
-      };
-      editable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Allow users to edit datasources from the UI.";
+        description = lib.mdDoc ''
+          Datasource specific secure configuration. Please note that the contents of this option
+          will end up in a world-readable Nix store. Use the file provider
+          pointing at a reasonably secured file in the local filesystem
+          to work around that. Look at the documentation for details:
+          <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+        '';
       };
     };
   };
 
   # http://docs.grafana.org/administration/provisioning/#dashboards
   grafanaTypes.dashboardConfig = types.submodule {
+    freeformType = provisioningSettingsFormat.type;
+
     options = {
       name = mkOption {
         type = types.str;
         default = "default";
-        description = lib.mdDoc "Provider name.";
-      };
-      orgId = mkOption {
-        type = types.int;
-        default = 1;
-        description = lib.mdDoc "Organization ID.";
-      };
-      folder = mkOption {
-        type = types.str;
-        default = "";
-        description = lib.mdDoc "Add dashboards to the specified folder.";
+        description = lib.mdDoc "A unique provider name.";
       };
       type = mkOption {
         type = types.str;
         default = "file";
         description = lib.mdDoc "Dashboard provider type.";
       };
-      disableDeletion = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Disable deletion when JSON file is removed.";
-      };
-      updateIntervalSeconds = mkOption {
-        type = types.int;
-        default = 10;
-        description = lib.mdDoc "How often Grafana will scan for changed dashboards.";
-      };
-      options = {
-        path = mkOption {
-          type = types.path;
-          description = lib.mdDoc "Path grafana will watch for dashboards.";
-        };
-        foldersFromFilesStructure = mkOption {
-          type = types.bool;
-          default = false;
-          description = lib.mdDoc "Use folder names from filesystem to create folders in Grafana.";
-        };
+      options.path = mkOption {
+        type = types.path;
+        description = lib.mdDoc "Path grafana will watch for dashboards. Required when using the 'file' type.";
       };
     };
   };
@@ -296,76 +204,85 @@ let
       secure_settings = mkOption {
         type = types.nullOr types.attrs;
         default = null;
-        description = lib.mdDoc "Secure settings for the notifier type.";
+        description = lib.mdDoc ''
+          Secure settings for the notifier type. Please note that the contents of this option
+          will end up in a world-readable Nix store. Use the file provider
+          pointing at a reasonably secured file in the local filesystem
+          to work around that. Look at the documentation for details:
+          <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+        '';
       };
     };
   };
 in {
+  imports = [
+    (mkRenamedOptionModule [ "services" "grafana" "protocol" ] [ "services" "grafana" "settings" "server" "protocol" ])
+    (mkRenamedOptionModule [ "services" "grafana" "addr" ] [ "services" "grafana" "settings" "server" "http_addr" ])
+    (mkRenamedOptionModule [ "services" "grafana" "port" ] [ "services" "grafana" "settings" "server" "http_port" ])
+    (mkRenamedOptionModule [ "services" "grafana" "domain" ] [ "services" "grafana" "settings" "server" "domain" ])
+    (mkRenamedOptionModule [ "services" "grafana" "rootUrl" ] [ "services" "grafana" "settings" "server" "root_url" ])
+    (mkRenamedOptionModule [ "services" "grafana" "staticRootPath" ] [ "services" "grafana" "settings" "server" "static_root_path" ])
+    (mkRenamedOptionModule [ "services" "grafana" "certFile" ] [ "services" "grafana" "settings" "server" "cert_file" ])
+    (mkRenamedOptionModule [ "services" "grafana" "certKey" ] [ "services" "grafana" "settings" "server" "cert_key" ])
+    (mkRenamedOptionModule [ "services" "grafana" "socket" ] [ "services" "grafana" "settings" "server" "socket" ])
+    (mkRenamedOptionModule [ "services" "grafana" "database" "type" ] [ "services" "grafana" "settings" "database" "type" ])
+    (mkRenamedOptionModule [ "services" "grafana" "database" "host" ] [ "services" "grafana" "settings" "database" "host" ])
+    (mkRenamedOptionModule [ "services" "grafana" "database" "name" ] [ "services" "grafana" "settings" "database" "name" ])
+    (mkRenamedOptionModule [ "services" "grafana" "database" "user" ] [ "services" "grafana" "settings" "database" "user" ])
+    (mkRenamedOptionModule [ "services" "grafana" "database" "password" ] [ "services" "grafana" "settings" "database" "password" ])
+    (mkRenamedOptionModule [ "services" "grafana" "database" "path" ] [ "services" "grafana" "settings" "database" "path" ])
+    (mkRenamedOptionModule [ "services" "grafana" "database" "connMaxLifetime" ] [ "services" "grafana" "settings" "database" "conn_max_lifetime" ])
+    (mkRenamedOptionModule [ "services" "grafana" "security" "adminUser" ] [ "services" "grafana" "settings" "security" "admin_user" ])
+    (mkRenamedOptionModule [ "services" "grafana" "security" "adminPassword" ] [ "services" "grafana" "settings" "security" "admin_password" ])
+    (mkRenamedOptionModule [ "services" "grafana" "security" "secretKey" ] [ "services" "grafana" "settings" "security" "secret_key" ])
+    (mkRenamedOptionModule [ "services" "grafana" "server" "serveFromSubPath" ] [ "services" "grafana" "settings" "server" "serve_from_sub_path" ])
+    (mkRenamedOptionModule [ "services" "grafana" "smtp" "enable" ] [ "services" "grafana" "settings" "smtp" "enabled" ])
+    (mkRenamedOptionModule [ "services" "grafana" "smtp" "user" ] [ "services" "grafana" "settings" "smtp" "user" ])
+    (mkRenamedOptionModule [ "services" "grafana" "smtp" "password" ] [ "services" "grafana" "settings" "smtp" "password" ])
+    (mkRenamedOptionModule [ "services" "grafana" "smtp" "fromAddress" ] [ "services" "grafana" "settings" "smtp" "from_address" ])
+    (mkRenamedOptionModule [ "services" "grafana" "users" "allowSignUp" ] [ "services" "grafana" "settings" "users" "allow_sign_up" ])
+    (mkRenamedOptionModule [ "services" "grafana" "users" "allowOrgCreate" ] [ "services" "grafana" "settings" "users" "allow_org_create" ])
+    (mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrg" ] [ "services" "grafana" "settings" "users" "auto_assign_org" ])
+    (mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrgRole" ] [ "services" "grafana" "settings" "users" "auto_assign_org_role" ])
+    (mkRenamedOptionModule [ "services" "grafana" "auth" "disableLoginForm" ] [ "services" "grafana" "settings" "auth" "disable_login_form" ])
+    (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "enable" ] [ "services" "grafana" "settings" "auth.anonymous" "enabled" ])
+    (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_name" ] [ "services" "grafana" "settings" "auth.anonymous" "org_name" ])
+    (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_role" ] [ "services" "grafana" "settings" "auth.anonymous" "org_role" ])
+    (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "enable" ] [ "services" "grafana" "settings" "auth.azuread" "enabled" ])
+    (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowSignUp" ] [ "services" "grafana" "settings" "auth.azuread" "allow_sign_up" ])
+    (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "clientId" ] [ "services" "grafana" "settings" "auth.azuread" "client_id" ])
+    (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedDomains" ] [ "services" "grafana" "settings" "auth.azuread" "allowed_domains" ])
+    (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedGroups" ] [ "services" "grafana" "settings" "auth.azuread" "allowed_groups" ])
+    (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "enable" ] [ "services" "grafana" "settings" "auth.google" "enabled" ])
+    (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "allowSignUp" ] [ "services" "grafana" "settings" "auth.google" "allow_sign_up" ])
+    (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "clientId" ] [ "services" "grafana" "settings" "auth.google" "client_id" ])
+    (mkRenamedOptionModule [ "services" "grafana" "analytics" "reporting" "enable" ] [ "services" "grafana" "settings" "analytics" "reporting_enabled" ])
+
+    (mkRemovedOptionModule [ "services" "grafana" "database" "passwordFile" ] ''
+      This option has been removed. Use 'services.grafana.settings.database.password' with file provider instead.
+    '')
+    (mkRemovedOptionModule [ "services" "grafana" "security" "adminPasswordFile" ] ''
+      This option has been removed. Use 'services.grafana.settings.security.admin_password' with file provider instead.
+    '')
+    (mkRemovedOptionModule [ "services" "grafana" "security" "secretKeyFile" ] ''
+      This option has been removed. Use 'services.grafana.settings.security.secret_key' with file provider instead.
+    '')
+    (mkRemovedOptionModule [ "services" "grafana" "smtp" "passwordFile" ] ''
+      This option has been removed. Use 'services.grafana.settings.smtp.password' with file provider instead.
+    '')
+    (mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "clientSecretFile" ] ''
+      This option has been removed. Use 'services.grafana.settings.azuread.client_secret' with file provider instead.
+    '')
+    (mkRemovedOptionModule [ "services" "grafana" "auth" "google" "clientSecretFile" ] ''
+      This option has been removed. Use 'services.grafana.settings.google.client_secret' with file provider instead.
+    '')
+
+    (mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "tenantId" ] "This option has been deprecated upstream.")
+  ];
+
   options.services.grafana = {
     enable = mkEnableOption (lib.mdDoc "grafana");
 
-    protocol = mkOption {
-      description = lib.mdDoc "Which protocol to listen.";
-      default = "http";
-      type = types.enum ["http" "https" "socket"];
-    };
-
-    addr = mkOption {
-      description = lib.mdDoc "Listening address.";
-      default = "127.0.0.1";
-      type = types.str;
-    };
-
-    port = mkOption {
-      description = lib.mdDoc "Listening port.";
-      default = 3000;
-      type = types.port;
-    };
-
-    socket = mkOption {
-      description = lib.mdDoc "Listening socket.";
-      default = "/run/grafana/grafana.sock";
-      type = types.str;
-    };
-
-    domain = mkOption {
-      description = lib.mdDoc "The public facing domain name used to access grafana from a browser.";
-      default = "localhost";
-      type = types.str;
-    };
-
-    rootUrl = mkOption {
-      description = lib.mdDoc "Full public facing url.";
-      default = "%(protocol)s://%(domain)s:%(http_port)s/";
-      type = types.str;
-    };
-
-    certFile = mkOption {
-      description = lib.mdDoc "Cert file for ssl.";
-      default = "";
-      type = types.str;
-    };
-
-    certKey = mkOption {
-      description = lib.mdDoc "Cert key for ssl.";
-      default = "";
-      type = types.str;
-    };
-
-    staticRootPath = mkOption {
-      description = lib.mdDoc "Root path for static assets.";
-      default = "${cfg.package}/share/grafana/public";
-      defaultText = literalExpression ''"''${package}/share/grafana/public"'';
-      type = types.str;
-    };
-
-    package = mkOption {
-      description = lib.mdDoc "Package to use.";
-      default = pkgs.grafana;
-      defaultText = literalExpression "pkgs.grafana";
-      type = types.package;
-    };
-
     declarativePlugins = mkOption {
       type = with types; nullOr (listOf path);
       default = null;
@@ -377,354 +294,952 @@ in {
       apply = x: if isList x then lib.unique x else x;
     };
 
+    package = mkOption {
+      description = lib.mdDoc "Package to use.";
+      default = pkgs.grafana;
+      defaultText = literalExpression "pkgs.grafana";
+      type = types.package;
+    };
+
     dataDir = mkOption {
       description = lib.mdDoc "Data directory.";
       default = "/var/lib/grafana";
       type = types.path;
     };
 
-    database = {
-      type = mkOption {
-        description = lib.mdDoc "Database type.";
-        default = "sqlite3";
-        type = types.enum ["mysql" "sqlite3" "postgres"];
-      };
-
-      host = mkOption {
-        description = lib.mdDoc "Database host.";
-        default = "127.0.0.1:3306";
-        type = types.str;
-      };
-
-      name = mkOption {
-        description = lib.mdDoc "Database name.";
-        default = "grafana";
-        type = types.str;
-      };
-
-      user = mkOption {
-        description = lib.mdDoc "Database user.";
-        default = "root";
-        type = types.str;
-      };
-
-      password = mkOption {
-        description = lib.mdDoc ''
-          Database password.
-          This option is mutual exclusive with the passwordFile option.
-        '';
-        default = "";
-        type = types.str;
-      };
-
-      passwordFile = mkOption {
-        description = lib.mdDoc ''
-          File that containts the database password.
-          This option is mutual exclusive with the password option.
-        '';
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      path = mkOption {
-        description = lib.mdDoc "Database path.";
-        default = "${cfg.dataDir}/data/grafana.db";
-        defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"'';
-        type = types.path;
-      };
-
-      connMaxLifetime = mkOption {
-        description = lib.mdDoc ''
-          Sets the maximum amount of time (in seconds) a connection may be reused.
-          For MySQL this setting should be shorter than the `wait_timeout' variable.
-        '';
-        default = "unlimited";
-        example = 14400;
-        type = types.either types.int (types.enum [ "unlimited" ]);
+    settings = mkOption {
+      description = lib.mdDoc ''
+        Grafana settings. See <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/>
+        for available options. INI format is used.
+      '';
+      type = types.submodule {
+        freeformType = settingsFormatIni.type;
+
+        options = {
+          paths = {
+            plugins = mkOption {
+              description = lib.mdDoc "Directory where grafana will automatically scan and look for plugins";
+              default = if (cfg.declarativePlugins == null) then "${cfg.dataDir}/plugins" else declarativePlugins;
+              defaultText = literalExpression "if (cfg.declarativePlugins == null) then \"\${cfg.dataDir}/plugins\" else declarativePlugins";
+              type = types.path;
+            };
+
+            provisioning = mkOption {
+              description = lib.mdDoc ''
+                Folder that contains provisioning config files that grafana will apply on startup and while running.
+                Don't change the value of this option if you are planning to use `services.grafana.provision` options.
+              '';
+              default = provisionConfDir;
+              defaultText = literalExpression ''
+                pkgs.runCommand "grafana-provisioning" { } \'\'
+                  mkdir -p $out/{datasources,dashboards,notifiers,alerting}
+                  ln -sf ''${datasourceFile} $out/datasources/datasource.yaml
+                  ln -sf ''${dashboardFile} $out/dashboards/dashboard.yaml
+                  ln -sf ''${notifierFile} $out/notifiers/notifier.yaml
+                  ln -sf ''${rulesFile} $out/alerting/rules.yaml
+                  ln -sf ''${contactPointsFile} $out/alerting/contactPoints.yaml
+                  ln -sf ''${policiesFile} $out/alerting/policies.yaml
+                  ln -sf ''${templatesFile} $out/alerting/templates.yaml
+                  ln -sf ''${muteTimingsFile} $out/alerting/muteTimings.yaml
+                  \'\'
+              '';
+              type = types.path;
+            };
+          };
+
+          server = {
+            protocol = mkOption {
+              description = lib.mdDoc "Which protocol to listen.";
+              default = "http";
+              type = types.enum ["http" "https" "h2" "socket"];
+            };
+
+            http_addr = mkOption {
+              description = lib.mdDoc "Listening address.";
+              default = "";
+              type = types.str;
+            };
+
+            http_port = mkOption {
+              description = lib.mdDoc "Listening port.";
+              default = 3000;
+              type = types.port;
+            };
+
+            domain = mkOption {
+              description = lib.mdDoc "The public facing domain name used to access grafana from a browser.";
+              default = "localhost";
+              type = types.str;
+            };
+
+            root_url = mkOption {
+              description = lib.mdDoc "Full public facing url.";
+              default = "%(protocol)s://%(domain)s:%(http_port)s/";
+              type = types.str;
+            };
+
+            static_root_path = mkOption {
+              description = lib.mdDoc "Root path for static assets.";
+              default = "${cfg.package}/share/grafana/public";
+              defaultText = literalExpression ''"''${package}/share/grafana/public"'';
+              type = types.str;
+            };
+
+            enable_gzip = mkOption {
+              description = lib.mdDoc ''
+                Set this option to true to enable HTTP compression, this can improve transfer speed and bandwidth utilization.
+                It is recommended that most users set it to true. By default it is set to false for compatibility reasons.
+              '';
+              default = false;
+              type = types.bool;
+            };
+
+            cert_file = mkOption {
+              description = lib.mdDoc "Cert file for ssl.";
+              default = "";
+              type = types.str;
+            };
+
+            cert_key = mkOption {
+              description = lib.mdDoc "Cert key for ssl.";
+              default = "";
+              type = types.str;
+            };
+
+            socket = mkOption {
+              description = lib.mdDoc "Path where the socket should be created when protocol=socket. Make sure that Grafana has appropriate permissions before you change this setting.";
+              default = "/run/grafana/grafana.sock";
+              type = types.str;
+            };
+          };
+
+          database = {
+            type = mkOption {
+              description = lib.mdDoc "Database type.";
+              default = "sqlite3";
+              type = types.enum ["mysql" "sqlite3" "postgres"];
+            };
+
+            host = mkOption {
+              description = lib.mdDoc "Database host.";
+              default = "127.0.0.1:3306";
+              type = types.str;
+            };
+
+            name = mkOption {
+              description = lib.mdDoc "Database name.";
+              default = "grafana";
+              type = types.str;
+            };
+
+            user = mkOption {
+              description = lib.mdDoc "Database user.";
+              default = "root";
+              type = types.str;
+            };
+
+            password = mkOption {
+              description = lib.mdDoc ''
+                Database password. Please note that the contents of this option
+                will end up in a world-readable Nix store. Use the file provider
+                pointing at a reasonably secured file in the local filesystem
+                to work around that. Look at the documentation for details:
+                <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+              '';
+              default = "";
+              type = types.str;
+            };
+
+            path = mkOption {
+              description = lib.mdDoc "Only applicable to sqlite3 database. The file path where the database will be stored.";
+              default = "${cfg.dataDir}/data/grafana.db";
+              defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"'';
+              type = types.path;
+            };
+          };
+
+          security = {
+            admin_user = mkOption {
+              description = lib.mdDoc "Default admin username.";
+              default = "admin";
+              type = types.str;
+            };
+
+            admin_password = mkOption {
+              description = lib.mdDoc ''
+                Default admin password. Please note that the contents of this option
+                will end up in a world-readable Nix store. Use the file provider
+                pointing at a reasonably secured file in the local filesystem
+                to work around that. Look at the documentation for details:
+                <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+              '';
+              default = "admin";
+              type = types.str;
+            };
+
+            secret_key = mkOption {
+              description = lib.mdDoc ''
+                Secret key used for signing. Please note that the contents of this option
+                will end up in a world-readable Nix store. Use the file provider
+                pointing at a reasonably secured file in the local filesystem
+                to work around that. Look at the documentation for details:
+                <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+              '';
+              default = "SW2YcwTIb9zpOOhoPsMm";
+              type = types.str;
+            };
+          };
+
+          smtp = {
+            enabled = mkOption {
+              description = lib.mdDoc "Whether to enable SMTP.";
+              default = false;
+              type = types.bool;
+            };
+            host = mkOption {
+              description = lib.mdDoc "Host to connect to.";
+              default = "localhost:25";
+              type = types.str;
+            };
+            user = mkOption {
+              description = lib.mdDoc "User used for authentication.";
+              default = "";
+              type = types.str;
+            };
+            password = mkOption {
+              description = lib.mdDoc ''
+                Password used for authentication. Please note that the contents of this option
+                will end up in a world-readable Nix store. Use the file provider
+                pointing at a reasonably secured file in the local filesystem
+                to work around that. Look at the documentation for details:
+                <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
+              '';
+              default = "";
+              type = types.str;
+            };
+            from_address = mkOption {
+              description = lib.mdDoc "Email address used for sending.";
+              default = "admin@grafana.localhost";
+              type = types.str;
+            };
+          };
+
+          users = {
+            allow_sign_up = mkOption {
+              description = lib.mdDoc "Disable user signup / registration.";
+              default = false;
+              type = types.bool;
+            };
+
+            allow_org_create = mkOption {
+              description = lib.mdDoc "Whether user is allowed to create organizations.";
+              default = false;
+              type = types.bool;
+            };
+
+            auto_assign_org = mkOption {
+              description = lib.mdDoc "Whether to automatically assign new users to default org.";
+              default = true;
+              type = types.bool;
+            };
+
+            auto_assign_org_role = mkOption {
+              description = lib.mdDoc "Default role new users will be auto assigned.";
+              default = "Viewer";
+              type = types.enum ["Viewer" "Editor"];
+            };
+          };
+
+          analytics.reporting_enabled = mkOption {
+            description = lib.mdDoc "Whether to allow anonymous usage reporting to stats.grafana.net.";
+            default = true;
+            type = types.bool;
+          };
+        };
       };
     };
 
     provision = {
       enable = mkEnableOption (lib.mdDoc "provision");
-      datasources = mkOption {
-        description = lib.mdDoc "Grafana datasources configuration.";
-        default = [];
-        type = types.listOf grafanaTypes.datasourceConfig;
-        apply = x: map _filter x;
-      };
-      dashboards = mkOption {
-        description = lib.mdDoc "Grafana dashboard configuration.";
-        default = [];
-        type = types.listOf grafanaTypes.dashboardConfig;
-        apply = x: map _filter x;
-      };
-      notifiers = mkOption {
-        description = lib.mdDoc "Grafana notifier configuration.";
-        default = [];
-        type = types.listOf grafanaTypes.notifierConfig;
-        apply = x: map _filter x;
-      };
-    };
-
-    security = {
-      adminUser = mkOption {
-        description = lib.mdDoc "Default admin username.";
-        default = "admin";
-        type = types.str;
-      };
-
-      adminPassword = mkOption {
-        description = lib.mdDoc ''
-          Default admin password.
-          This option is mutual exclusive with the adminPasswordFile option.
-        '';
-        default = "admin";
-        type = types.str;
-      };
 
-      adminPasswordFile = mkOption {
+      datasources = mkOption {
         description = lib.mdDoc ''
-          Default admin password.
-          This option is mutual exclusive with the `adminPassword` option.
+          Deprecated option for Grafana datasource configuration. Use either
+          `services.grafana.provision.datasources.settings` or
+          `services.grafana.provision.datasources.path` instead.
         '';
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      secretKey = mkOption {
-        description = lib.mdDoc "Secret key used for signing.";
-        default = "SW2YcwTIb9zpOOhoPsMm";
-        type = types.str;
-      };
+        default = [];
+        apply = x: if (builtins.isList x) then map _filter x else x;
+        type = with types; either (listOf grafanaTypes.datasourceConfig) (submodule {
+          options.settings = mkOption {
+            description = lib.mdDoc ''
+              Grafana datasource configuration in Nix. Can't be used with
+              `services.grafana.provision.datasources.path` simultaneously. See
+              <https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources>
+              for supported options.
+            '';
+            default = null;
+            type = types.nullOr (types.submodule {
+              options = {
+                apiVersion = mkOption {
+                  description = lib.mdDoc "Config file version.";
+                  default = 1;
+                  type = types.int;
+                };
+
+                datasources = mkOption {
+                  description = lib.mdDoc "List of datasources to insert/update.";
+                  default = [];
+                  type = types.listOf grafanaTypes.datasourceConfig;
+                };
+
+                deleteDatasources = mkOption {
+                  description = lib.mdDoc "List of datasources that should be deleted from the database.";
+                  default = [];
+                  type = types.listOf (types.submodule {
+                    options.name = mkOption {
+                      description = lib.mdDoc "Name of the datasource to delete.";
+                      type = types.str;
+                    };
+
+                    options.orgId = mkOption {
+                      description = lib.mdDoc "Organization ID of the datasource to delete.";
+                      type = types.int;
+                    };
+                  });
+                };
+              };
+            });
+            example = literalExpression ''
+              {
+                apiVersion = 1;
+
+                datasources = [{
+                  name = "Graphite";
+                  type = "graphite";
+                }];
+
+                deleteDatasources = [{
+                  name = "Graphite";
+                  orgId = 1;
+                }];
+              }
+            '';
+          };
 
-      secretKeyFile = mkOption {
-        description = lib.mdDoc "Secret key used for signing.";
-        default = null;
-        type = types.nullOr types.path;
+          options.path = mkOption {
+            description = lib.mdDoc ''
+              Path to YAML datasource configuration. Can't be used with
+              `services.grafana.provision.datasources.settings` simultaneously.
+            '';
+            default = null;
+            type = types.nullOr types.path;
+          };
+        });
       };
-    };
 
-    server = {
-      serveFromSubPath = mkOption {
-        description = lib.mdDoc "Serve Grafana from subpath specified in rootUrl setting";
-        default = false;
-        type = types.bool;
-      };
-    };
 
-    smtp = {
-      enable = mkEnableOption (lib.mdDoc "smtp");
-      host = mkOption {
-        description = lib.mdDoc "Host to connect to.";
-        default = "localhost:25";
-        type = types.str;
-      };
-      user = mkOption {
-        description = lib.mdDoc "User used for authentication.";
-        default = "";
-        type = types.str;
-      };
-      password = mkOption {
-        description = lib.mdDoc ''
-          Password used for authentication.
-          This option is mutual exclusive with the passwordFile option.
-        '';
-        default = "";
-        type = types.str;
-      };
-      passwordFile = mkOption {
+      dashboards = mkOption {
         description = lib.mdDoc ''
-          Password used for authentication.
-          This option is mutual exclusive with the password option.
+          Deprecated option for Grafana dashboard configuration. Use either
+          `services.grafana.provision.dashboards.settings` or
+          `services.grafana.provision.dashboards.path` instead.
         '';
-        default = null;
-        type = types.nullOr types.path;
-      };
-      fromAddress = mkOption {
-        description = lib.mdDoc "Email address used for sending.";
-        default = "admin@grafana.localhost";
-        type = types.str;
-      };
-    };
-
-    users = {
-      allowSignUp = mkOption {
-        description = lib.mdDoc "Disable user signup / registration.";
-        default = false;
-        type = types.bool;
-      };
+        default = [];
+        apply = x: if (builtins.isList x) then map _filter x else x;
+        type = with types; either (listOf grafanaTypes.dashboardConfig) (submodule {
+          options.settings = mkOption {
+            description = lib.mdDoc ''
+              Grafana dashboard configuration in Nix. Can't be used with
+              `services.grafana.provision.dashboards.path` simultaneously. See
+              <https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards>
+              for supported options.
+            '';
+            default = null;
+            type = types.nullOr (types.submodule {
+              options.apiVersion = mkOption {
+                description = lib.mdDoc "Config file version.";
+                default = 1;
+                type = types.int;
+              };
+
+              options.providers = mkOption {
+                description = lib.mdDoc "List of dashboards to insert/update.";
+                default = [];
+                type = types.listOf grafanaTypes.dashboardConfig;
+              };
+            });
+            example = literalExpression ''
+              {
+                apiVersion = 1;
+
+                providers = [{
+                    name = "default";
+                    options.path = "/var/lib/grafana/dashboards";
+                }];
+              }
+            '';
+          };
 
-      allowOrgCreate = mkOption {
-        description = lib.mdDoc "Whether user is allowed to create organizations.";
-        default = false;
-        type = types.bool;
+          options.path = mkOption {
+            description = lib.mdDoc ''
+              Path to YAML dashboard configuration. Can't be used with
+              `services.grafana.provision.dashboards.settings` simultaneously.
+            '';
+            default = null;
+            type = types.nullOr types.path;
+          };
+        });
       };
 
-      autoAssignOrg = mkOption {
-        description = lib.mdDoc "Whether to automatically assign new users to default org.";
-        default = true;
-        type = types.bool;
-      };
 
-      autoAssignOrgRole = mkOption {
-        description = lib.mdDoc "Default role new users will be auto assigned.";
-        default = "Viewer";
-        type = types.enum ["Viewer" "Editor"];
+      notifiers = mkOption {
+        description = lib.mdDoc "Grafana notifier configuration.";
+        default = [];
+        type = types.listOf grafanaTypes.notifierConfig;
+        apply = x: map _filter x;
       };
-    };
 
-    auth = {
-      disableLoginForm = mkOption {
-        description = lib.mdDoc "Set to true to disable (hide) the login form, useful if you use OAuth";
-        default = false;
-        type = types.bool;
-      };
 
-      anonymous = {
-        enable = mkOption {
-          description = lib.mdDoc "Whether to allow anonymous access.";
-          default = false;
-          type = types.bool;
-        };
-        org_name = mkOption {
-          description = lib.mdDoc "Which organization to allow anonymous access to.";
-          default = "Main Org.";
-          type = types.str;
-        };
-        org_role = mkOption {
-          description = lib.mdDoc "Which role anonymous users have in the organization.";
-          default = "Viewer";
-          type = types.str;
-        };
-      };
-      azuread = {
-        enable = mkOption {
-          description = lib.mdDoc "Whether to allow Azure AD OAuth.";
-          default = false;
-          type = types.bool;
-        };
-        allowSignUp = mkOption {
-          description = lib.mdDoc "Whether to allow sign up with Azure AD OAuth.";
-          default = false;
-          type = types.bool;
-        };
-        clientId = mkOption {
-          description = lib.mdDoc "Azure AD OAuth client ID.";
-          default = "";
-          type = types.str;
-        };
-        clientSecretFile = mkOption {
-          description = lib.mdDoc "Azure AD OAuth client secret.";
-          default = null;
-          type = types.nullOr types.path;
-        };
-        tenantId = mkOption {
-          description = lib.mdDoc ''
-            Tenant id used to create auth and token url. Default to "common"
-            , let user sign in with any tenant.
+      alerting = {
+        rules = {
+          path = mkOption {
+            description = lib.mdDoc ''
+              Path to YAML rules configuration. Can't be used with
+              `services.grafana.provision.alerting.rules.settings` simultaneously.
             '';
-          default = "common";
-          type = types.str;
-        };
-        allowedDomains = mkOption {
-          description = lib.mdDoc ''
-            Limits access to users who belong to specific domains.
-            Separate domains with space or comma.
-          '';
-          default = "";
-          type = types.str;
-        };
-        allowedGroups = mkOption {
-          description = lib.mdDoc ''
-            To limit access to authenticated users who are members of one or more groups,
-            set allowedGroups to a comma- or space-separated list of group object IDs.
-            You can find object IDs for a specific group on the Azure portal.
+            default = null;
+            type = types.nullOr types.path;
+          };
+
+          settings = mkOption {
+            description = lib.mdDoc ''
+              Grafana rules configuration in Nix. Can't be used with
+              `services.grafana.provision.alerting.rules.path` simultaneously. See
+              <https://grafana.com/docs/grafana/latest/administration/provisioning/#rules>
+              for supported options.
             '';
-          default = "";
-          type = types.str;
-        };
-      };
-      google = {
-        enable = mkOption {
-          description = lib.mdDoc "Whether to allow Google OAuth2.";
-          default = false;
-          type = types.bool;
+            default = null;
+            type = types.nullOr (types.submodule {
+              options = {
+                apiVersion = mkOption {
+                  description = lib.mdDoc "Config file version.";
+                  default = 1;
+                  type = types.int;
+                };
+
+                groups = mkOption {
+                  description = lib.mdDoc "List of rule groups to import or update.";
+                  default = [];
+                  type = types.listOf (types.submodule {
+                    freeformType = provisioningSettingsFormat.type;
+
+                    options.name = mkOption {
+                      description = lib.mdDoc "Name of the rule group. Required.";
+                      type = types.str;
+                    };
+
+                    options.folder = mkOption {
+                      description = lib.mdDoc "Name of the folder the rule group will be stored in. Required.";
+                      type = types.str;
+                    };
+
+                    options.interval = mkOption {
+                      description = lib.mdDoc "Interval that the rule group should be evaluated at. Required.";
+                      type = types.str;
+                    };
+                  });
+                };
+
+                deleteRules = mkOption {
+                  description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
+                  default = [];
+                  type = types.listOf (types.submodule {
+                    options.orgId = mkOption {
+                      description = lib.mdDoc "Organization ID, default = 1";
+                      default = 1;
+                      type = types.int;
+                    };
+
+                    options.uid = mkOption {
+                      description = lib.mdDoc "Unique identifier for the rule. Required.";
+                      type = types.str;
+                    };
+                  });
+                };
+              };
+            });
+            example = literalExpression ''
+              {
+                apiVersion = 1;
+
+                groups = [{
+                  orgId = 1;
+                  name = "my_rule_group";
+                  folder = "my_first_folder";
+                  interval = "60s";
+                  rules = [{
+                    uid = "my_id_1";
+                    title = "my_first_rule";
+                    condition = "A";
+                    data = [{
+                      refId = "A";
+                      datasourceUid = "-100";
+                      model = {
+                        conditions = [{
+                          evaluator = {
+                            params = [ 3 ];
+                            type = "git";
+                          };
+                          operator.type = "and";
+                          query.params = [ "A" ];
+                          reducer.type = "last";
+                          type = "query";
+                        }];
+                        datasource = {
+                          type = "__expr__";
+                          uid = "-100";
+                        };
+                        expression = "1==0";
+                        intervalMs = 1000;
+                        maxDataPoints = 43200;
+                        refId = "A";
+                        type = "math";
+                      };
+                    }];
+                    dashboardUid = "my_dashboard";
+                    panelId = 123;
+                    noDataState = "Alerting";
+                    for = "60s";
+                    annotations.some_key = "some_value";
+                    labels.team = "sre_team1";
+                  }];
+                }];
+
+                deleteRules = [{
+                  orgId = 1;
+                  uid = "my_id_1";
+                }];
+              }
+            '';
+          };
         };
-        allowSignUp = mkOption {
-          description = lib.mdDoc "Whether to allow sign up with Google OAuth2.";
-          default = false;
-          type = types.bool;
+
+        contactPoints = {
+          path = mkOption {
+            description = lib.mdDoc ''
+              Path to YAML contact points configuration. Can't be used with
+              `services.grafana.provision.alerting.contactPoints.settings` simultaneously.
+            '';
+            default = null;
+            type = types.nullOr types.path;
+          };
+
+          settings = mkOption {
+            description = lib.mdDoc ''
+              Grafana contact points configuration in Nix. Can't be used with
+              `services.grafana.provision.alerting.contactPoints.path` simultaneously. See
+              <https://grafana.com/docs/grafana/latest/administration/provisioning/#contact-points>
+              for supported options.
+            '';
+            default = null;
+            type = types.nullOr (types.submodule {
+              options = {
+                apiVersion = mkOption {
+                  description = lib.mdDoc "Config file version.";
+                  default = 1;
+                  type = types.int;
+                };
+
+                contactPoints = mkOption {
+                  description = lib.mdDoc "List of contact points to import or update. Please note that sensitive data will end up in world-readable Nix store.";
+                  default = [];
+                  type = types.listOf (types.submodule {
+                    freeformType = provisioningSettingsFormat.type;
+
+                    options.name = mkOption {
+                      description = lib.mdDoc "Name of the contact point. Required.";
+                      type = types.str;
+                    };
+                  });
+                };
+
+                deleteContactPoints = mkOption {
+                  description = lib.mdDoc "List of receivers that should be deleted.";
+                  default = [];
+                  type = types.listOf (types.submodule {
+                    options.orgId = mkOption {
+                      description = lib.mdDoc "Organization ID, default = 1.";
+                      default = 1;
+                      type = types.int;
+                    };
+
+                    options.uid = mkOption {
+                      description = lib.mdDoc "Unique identifier for the receiver. Required.";
+                      type = types.str;
+                    };
+                  });
+                };
+              };
+            });
+            example = literalExpression ''
+              {
+                apiVersion = 1;
+
+                contactPoints = [{
+                  orgId = 1;
+                  name = "cp_1";
+                  receivers = [{
+                    uid = "first_uid";
+                    type = "prometheus-alertmanager";
+                    settings.url = "http://test:9000";
+                  }];
+                }];
+
+                deleteContactPoints = [{
+                  orgId = 1;
+                  uid = "first_uid";
+                }];
+              }
+            '';
+          };
         };
-        clientId = mkOption {
-          description = lib.mdDoc "Google OAuth2 client ID.";
-          default = "";
-          type = types.str;
+
+        policies = {
+          path = mkOption {
+            description = lib.mdDoc ''
+              Path to YAML notification policies configuration. Can't be used with
+              `services.grafana.provision.alerting.policies.settings` simultaneously.
+            '';
+            default = null;
+            type = types.nullOr types.path;
+          };
+
+          settings = mkOption {
+            description = lib.mdDoc ''
+              Grafana notification policies configuration in Nix. Can't be used with
+              `services.grafana.provision.alerting.policies.path` simultaneously. See
+              <https://grafana.com/docs/grafana/latest/administration/provisioning/#notification-policies>
+              for supported options.
+            '';
+            default = null;
+            type = types.nullOr (types.submodule {
+              options = {
+                apiVersion = mkOption {
+                  description = lib.mdDoc "Config file version.";
+                  default = 1;
+                  type = types.int;
+                };
+
+                policies = mkOption {
+                  description = lib.mdDoc "List of contact points to import or update.";
+                  default = [];
+                  type = types.listOf (types.submodule {
+                    freeformType = provisioningSettingsFormat.type;
+                  });
+                };
+
+                resetPolicies = mkOption {
+                  description = lib.mdDoc "List of orgIds that should be reset to the default policy.";
+                  default = [];
+                  type = types.listOf types.int;
+                };
+              };
+            });
+            example = literalExpression ''
+              {
+                apiVersion = 1;
+
+                policies = [{
+                  orgId = 1;
+                  receiver = "grafana-default-email";
+                  group_by = [ "..." ];
+                  matchers = [
+                    "alertname = Watchdog"
+                    "severity =~ \"warning|critical\""
+                  ];
+                  mute_time_intervals = [
+                    "abc"
+                  ];
+                  group_wait = "30s";
+                  group_interval = "5m";
+                  repeat_interval = "4h";
+                }];
+
+                resetPolicies = [
+                  1
+                ];
+              }
+            '';
+          };
         };
-        clientSecretFile = mkOption {
-          description = lib.mdDoc "Google OAuth2 client secret.";
-          default = null;
-          type = types.nullOr types.path;
+
+        templates = {
+          path = mkOption {
+            description = lib.mdDoc ''
+              Path to YAML templates configuration. Can't be used with
+              `services.grafana.provision.alerting.templates.settings` simultaneously.
+            '';
+            default = null;
+            type = types.nullOr types.path;
+          };
+
+          settings = mkOption {
+            description = lib.mdDoc ''
+              Grafana templates configuration in Nix. Can't be used with
+              `services.grafana.provision.alerting.templates.path` simultaneously. See
+              <https://grafana.com/docs/grafana/latest/administration/provisioning/#templates>
+              for supported options.
+            '';
+            default = null;
+            type = types.nullOr (types.submodule {
+              options = {
+                apiVersion = mkOption {
+                  description = lib.mdDoc "Config file version.";
+                  default = 1;
+                  type = types.int;
+                };
+
+                templates = mkOption {
+                  description = lib.mdDoc "List of templates to import or update.";
+                  default = [];
+                  type = types.listOf (types.submodule {
+                    freeformType = provisioningSettingsFormat.type;
+
+                    options.name = mkOption {
+                      description = lib.mdDoc "Name of the template, must be unique. Required.";
+                      type = types.str;
+                    };
+
+                    options.template = mkOption {
+                      description = lib.mdDoc "Alerting with a custom text template";
+                      type = types.str;
+                    };
+                  });
+                };
+
+                deleteTemplates = mkOption {
+                  description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
+                  default = [];
+                  type = types.listOf (types.submodule {
+                    options.orgId = mkOption {
+                      description = lib.mdDoc "Organization ID, default = 1.";
+                      default = 1;
+                      type = types.int;
+                    };
+
+                    options.name = mkOption {
+                      description = lib.mdDoc "Name of the template, must be unique. Required.";
+                      type = types.str;
+                    };
+                  });
+                };
+              };
+            });
+            example = literalExpression ''
+              {
+                apiVersion = 1;
+
+                templates = [{
+                  orgId = 1;
+                  name = "my_first_template";
+                  template = "Alerting with a custom text template";
+                }];
+
+                deleteTemplates = [{
+                  orgId = 1;
+                  name = "my_first_template";
+                }];
+              }
+            '';
+          };
         };
-      };
-    };
 
-    analytics.reporting = {
-      enable = mkOption {
-        description = lib.mdDoc "Whether to allow anonymous usage reporting to stats.grafana.net.";
-        default = true;
-        type = types.bool;
+        muteTimings = {
+          path = mkOption {
+            description = lib.mdDoc ''
+              Path to YAML mute timings configuration. Can't be used with
+              `services.grafana.provision.alerting.muteTimings.settings` simultaneously.
+            '';
+            default = null;
+            type = types.nullOr types.path;
+          };
+
+          settings = mkOption {
+            description = lib.mdDoc ''
+              Grafana mute timings configuration in Nix. Can't be used with
+              `services.grafana.provision.alerting.muteTimings.path` simultaneously. See
+              <https://grafana.com/docs/grafana/latest/administration/provisioning/#mute-timings>
+              for supported options.
+            '';
+            default = null;
+            type = types.nullOr (types.submodule {
+              options = {
+                apiVersion = mkOption {
+                  description = lib.mdDoc "Config file version.";
+                  default = 1;
+                  type = types.int;
+                };
+
+                muteTimes = mkOption {
+                  description = lib.mdDoc "List of mute time intervals to import or update.";
+                  default = [];
+                  type = types.listOf (types.submodule {
+                    freeformType = provisioningSettingsFormat.type;
+
+                    options.name = mkOption {
+                      description = lib.mdDoc "Name of the mute time interval, must be unique. Required.";
+                      type = types.str;
+                    };
+                  });
+                };
+
+                deleteMuteTimes = mkOption {
+                  description = lib.mdDoc "List of mute time intervals that should be deleted.";
+                  default = [];
+                  type = types.listOf (types.submodule {
+                    options.orgId = mkOption {
+                      description = lib.mdDoc "Organization ID, default = 1.";
+                      default = 1;
+                      type = types.int;
+                    };
+
+                    options.name = mkOption {
+                      description = lib.mdDoc "Name of the mute time interval, must be unique. Required.";
+                      type = types.str;
+                    };
+                  });
+                };
+              };
+            });
+            example = literalExpression ''
+              {
+                apiVersion = 1;
+
+                muteTimes = [{
+                  orgId = 1;
+                  name = "mti_1";
+                  time_intervals = [{
+                    times = [{
+                      start_time = "06:00";
+                      end_time = "23:59";
+                    }];
+                    weekdays = [
+                      "monday:wednesday"
+                      "saturday"
+                      "sunday"
+                    ];
+                    months = [
+                      "1:3"
+                      "may:august"
+                      "december"
+                    ];
+                    years = [
+                      "2020:2022"
+                      "2030"
+                    ];
+                    days_of_month = [
+                      "1:5"
+                      "-3:-1"
+                    ];
+                  }];
+                }];
+
+                deleteMuteTimes = [{
+                  orgId = 1;
+                  name = "mti_1";
+                }];
+              }
+            '';
+          };
+        };
       };
     };
-
-    extraOptions = mkOption {
-      description = lib.mdDoc ''
-        Extra configuration options passed as env variables as specified in
-        [documentation](http://docs.grafana.org/installation/configuration/),
-        but without GF_ prefix
-      '';
-      default = {};
-      type = with types; attrsOf (either str path);
-    };
   };
 
   config = mkIf cfg.enable {
-    warnings = flatten [
+    warnings = let
+      usesFileProvider = opt: defaultValue: builtins.match "^${defaultValue}$|^\\$__file\\{.*}$" opt != null;
+    in flatten [
       (optional (
-        cfg.database.password != opt.database.password.default ||
-        cfg.security.adminPassword != opt.security.adminPassword.default
-      ) "Grafana passwords will be stored as plaintext in the Nix store!")
+        ! usesFileProvider cfg.settings.database.password "" ||
+        ! usesFileProvider cfg.settings.security.admin_password "admin"
+      ) "Grafana passwords will be stored as plaintext in the Nix store! Use file provider instead.")
       (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!")
+        let
+          checkOpts = opt: any (x: x.password != null || x.basicAuthPassword != null || x.secureJsonData != null) opt;
+          datasourcesUsed = if (cfg.provision.datasources.settings == null) then [] else cfg.provision.datasources.settings.datasources;
+        in if (builtins.isList cfg.provision.datasources) then checkOpts cfg.provision.datasources else checkOpts datasourcesUsed
+        ) ''
+          Datasource passwords will be stored as plaintext in the Nix store!
+          It is not possible to use file provider in provisioning; please provision
+          datasources via `services.grafana.provision.datasources.path` instead.
+        '')
       (optional (
         any (x: x.secure_settings != null) cfg.provision.notifiers
-      ) "Notifier secure settings will be stored as plaintext in the Nix store!")
+      ) "Notifier secure settings will be stored as plaintext in the Nix store! Use file provider instead.")
+      (optional (
+        builtins.isList cfg.provision.datasources && cfg.provision.datasources != []
+      ) ''
+          Provisioning Grafana datasources with options has been deprecated.
+          Use `services.grafana.provision.datasources.settings` or
+          `services.grafana.provision.datasources.path` instead.
+        '')
+      (optional (
+        builtins.isList cfg.provision.datasources && cfg.provision.dashboards != []
+      ) ''
+          Provisioning Grafana dashboards with options has been deprecated.
+          Use `services.grafana.provision.dashboards.settings` or
+          `services.grafana.provision.dashboards.path` instead.
+        '')
+      (optional (
+        cfg.provision.notifiers != []
+        ) ''
+            Notifiers are deprecated upstream and will be removed in Grafana 10.
+            Use `services.grafana.provision.alerting.contactPoints` instead.
+        '')
     ];
 
     environment.systemPackages = [ cfg.package ];
 
     assertions = [
       {
-        assertion = cfg.database.password != opt.database.password.default -> cfg.database.passwordFile == null;
-        message = "Cannot set both password and passwordFile";
+        assertion = if (builtins.isList cfg.provision.datasources) then true else cfg.provision.datasources.settings == null || cfg.provision.datasources.path == null;
+        message = "Cannot set both datasources settings and datasources path";
       }
       {
-        assertion = cfg.security.adminPassword != opt.security.adminPassword.default -> cfg.security.adminPasswordFile == null;
-        message = "Cannot set both adminPassword and adminPasswordFile";
+        assertion = let
+          prometheusIsNotDirect = opt: all
+          ({ type, access, ... }: type == "prometheus" -> access != "direct")
+          opt;
+        in
+          if (builtins.isList cfg.provision.datasources) then prometheusIsNotDirect cfg.provision.datasources
+          else cfg.provision.datasources.settings == null || prometheusIsNotDirect cfg.provision.datasources.settings.datasources;
+        message = "For datasources of type `prometheus`, the `direct` access mode is not supported anymore (since Grafana 9.2.0)";
       }
       {
-        assertion = cfg.security.secretKey != opt.security.secretKey.default -> cfg.security.secretKeyFile == null;
-        message = "Cannot set both secretKey and secretKeyFile";
+        assertion = if (builtins.isList cfg.provision.dashboards) then true else cfg.provision.dashboards.settings == null || cfg.provision.dashboards.path == null;
+        message = "Cannot set both dashboards settings and dashboards path";
       }
       {
-        assertion = cfg.smtp.password != opt.smtp.password.default -> cfg.smtp.passwordFile == null;
-        message = "Cannot set both password and passwordFile";
+        assertion = cfg.provision.alerting.rules.settings == null || cfg.provision.alerting.rules.path == null;
+        message = "Cannot set both rules settings and rules path";
       }
       {
-        assertion = all
-          ({ type, access, ... }: type == "prometheus" -> access != "direct")
-          cfg.provision.datasources;
-        message = "For datasources of type `prometheus`, the `direct` access mode is not supported anymore (since Grafana 9.2.0)";
+        assertion = cfg.provision.alerting.contactPoints.settings == null || cfg.provision.alerting.contactPoints.path == null;
+        message = "Cannot set both contact points settings and contact points path";
+      }
+      {
+        assertion = cfg.provision.alerting.policies.settings == null || cfg.provision.alerting.policies.path == null;
+        message = "Cannot set both policies settings and policies path";
+      }
+      {
+        assertion = cfg.provision.alerting.templates.settings == null || cfg.provision.alerting.templates.path == null;
+        message = "Cannot set both templates settings and templates path";
+      }
+      {
+        assertion = cfg.provision.alerting.muteTimings.settings == null || cfg.provision.alerting.muteTimings.path == null;
+        message = "Cannot set both mute timings settings and mute timings path";
       }
     ];
 
@@ -732,41 +1247,11 @@ in {
       description = "Grafana Service Daemon";
       wantedBy = ["multi-user.target"];
       after = ["networking.target"] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
-      environment = {
-        QT_QPA_PLATFORM = "offscreen";
-      } // mapAttrs' (n: v: nameValuePair "GF_${n}" (toString v)) envOptions;
       script = ''
         set -o errexit -o pipefail -o nounset -o errtrace
         shopt -s inherit_errexit
 
-        ${optionalString (cfg.auth.azuread.clientSecretFile != null) ''
-          GF_AUTH_AZUREAD_CLIENT_SECRET="$(<${escapeShellArg cfg.auth.azuread.clientSecretFile})"
-          export GF_AUTH_AZUREAD_CLIENT_SECRET
-        ''}
-        ${optionalString (cfg.auth.google.clientSecretFile != null) ''
-          GF_AUTH_GOOGLE_CLIENT_SECRET="$(<${escapeShellArg cfg.auth.google.clientSecretFile})"
-          export GF_AUTH_GOOGLE_CLIENT_SECRET
-        ''}
-        ${optionalString (cfg.database.passwordFile != null) ''
-          GF_DATABASE_PASSWORD="$(<${escapeShellArg cfg.database.passwordFile})"
-          export GF_DATABASE_PASSWORD
-        ''}
-        ${optionalString (cfg.security.adminPasswordFile != null) ''
-          GF_SECURITY_ADMIN_PASSWORD="$(<${escapeShellArg cfg.security.adminPasswordFile})"
-          export GF_SECURITY_ADMIN_PASSWORD
-        ''}
-        ${optionalString (cfg.security.secretKeyFile != null) ''
-          GF_SECURITY_SECRET_KEY="$(<${escapeShellArg cfg.security.secretKeyFile})"
-          export GF_SECURITY_SECRET_KEY
-        ''}
-        ${optionalString (cfg.smtp.passwordFile != null) ''
-          GF_SMTP_PASSWORD="$(<${escapeShellArg cfg.smtp.passwordFile})"
-          export GF_SMTP_PASSWORD
-        ''}
-        ${optionalString cfg.provision.enable ''
-          export GF_PATHS_PROVISIONING=${provisionConfDir};
-        ''}
-        exec ${cfg.package}/bin/grafana-server -homepath ${cfg.dataDir}
+        exec ${cfg.package}/bin/grafana-server -homepath ${cfg.dataDir} -config ${configFile}
       '';
       serviceConfig = {
         WorkingDirectory = cfg.dataDir;
@@ -774,8 +1259,8 @@ in {
         RuntimeDirectory = "grafana";
         RuntimeDirectoryMode = "0755";
         # Hardening
-        AmbientCapabilities = lib.mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
-        CapabilityBoundingSet = if (cfg.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ];
+        AmbientCapabilities = lib.mkIf (cfg.settings.server.http_port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = if (cfg.settings.server.http_port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ];
         DeviceAllow = [ "" ];
         LockPersonality = true;
         NoNewPrivileges = true;
diff --git a/nixos/modules/services/monitoring/heapster.nix b/nixos/modules/services/monitoring/heapster.nix
index 2f2467477ae2f..fc63276b62f73 100644
--- a/nixos/modules/services/monitoring/heapster.nix
+++ b/nixos/modules/services/monitoring/heapster.nix
@@ -6,11 +6,7 @@ let
   cfg = config.services.heapster;
 in {
   options.services.heapster = {
-    enable = mkOption {
-      description = lib.mdDoc "Whether to enable heapster monitoring";
-      default = false;
-      type = types.bool;
-    };
+    enable = mkEnableOption (lib.mdDoc "Heapster monitoring");
 
     source = mkOption {
       description = lib.mdDoc "Heapster metric source";
diff --git a/nixos/modules/services/monitoring/karma.nix b/nixos/modules/services/monitoring/karma.nix
new file mode 100644
index 0000000000000..85dbc81f443f0
--- /dev/null
+++ b/nixos/modules/services/monitoring/karma.nix
@@ -0,0 +1,128 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.services.karma;
+  yaml = pkgs.formats.yaml { };
+in
+{
+  options.services.karma = {
+    enable = mkEnableOption (mdDoc "the Karma dashboard service");
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.karma;
+      defaultText = literalExpression "pkgs.karma";
+      description = mdDoc ''
+        The Karma package that should be used.
+      '';
+    };
+
+    configFile = mkOption {
+      type = types.path;
+      default = yaml.generate "karma.yaml" cfg.settings;
+      defaultText = "A configuration file generated from the provided nix attributes settings option.";
+      description = mdDoc ''
+        A YAML config file which can be used to configure karma instead of the nix-generated file.
+      '';
+      example = "/etc/karma/karma.conf";
+    };
+
+    environment = mkOption {
+      type = with types; attrsOf str;
+      default = {};
+      description = mdDoc ''
+        Additional environment variables to provide to karma.
+      '';
+      example = {
+        ALERTMANAGER_URI = "https://alertmanager.example.com";
+        ALERTMANAGER_NAME= "single";
+      };
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = mdDoc ''
+        Whether to open ports in the firewall needed for karma to function.
+      '';
+    };
+
+    extraOptions = mkOption {
+      type = with types; listOf str;
+      default = [];
+      description = mdDoc ''
+        Extra command line options.
+      '';
+      example = [
+        "--alertmanager.timeout 10s"
+      ];
+    };
+
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = yaml.type;
+
+        options.listen = {
+          address = mkOption {
+            type = types.str;
+            default = "127.0.0.1";
+            description = mdDoc ''
+              Hostname or IP to listen on.
+            '';
+            example = "[::]";
+          };
+
+          port = mkOption {
+            type = types.port;
+            default = 8080;
+            description = mdDoc ''
+              HTTP port to listen on.
+            '';
+            example = 8182;
+          };
+        };
+      };
+      default = {
+        listen = {
+          address = "127.0.0.1";
+        };
+      };
+      description = mdDoc ''
+        Karma dashboard configuration as nix attributes.
+
+        Reference: <https://github.com/prymitive/karma/blob/main/docs/CONFIGURATION.md>
+      '';
+      example = {
+        listen = {
+          address = "192.168.1.4";
+          port = "8000";
+          prefix = "/dashboard";
+        };
+        alertmanager = {
+          interval = "15s";
+          servers = [
+            {
+              name = "prod";
+              uri = "http://alertmanager.example.com";
+            }
+          ];
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.karma = {
+      description = "Alert dashboard for Prometheus Alertmanager";
+      wantedBy = [ "multi-user.target" ];
+      environment = cfg.environment;
+      serviceConfig = {
+        Type = "simple";
+        DynamicUser = true;
+        Restart = "on-failure";
+        ExecStart = "${pkgs.karma}/bin/karma --config.file ${cfg.configFile} ${concatStringsSep " " cfg.extraOptions}";
+      };
+    };
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.listen.port ];
+  };
+}
diff --git a/nixos/modules/services/monitoring/parsedmarc.nix b/nixos/modules/services/monitoring/parsedmarc.nix
index 7618414d9040f..3540d91fc9f37 100644
--- a/nixos/modules/services/monitoring/parsedmarc.nix
+++ b/nixos/modules/services/monitoring/parsedmarc.nix
@@ -494,7 +494,7 @@ in
             Group = "parsedmarc";
             DynamicUser = true;
             RuntimeDirectory = "parsedmarc";
-            RuntimeDirectoryMode = 0700;
+            RuntimeDirectoryMode = "0700";
             CapabilityBoundingSet = "";
             PrivateDevices = true;
             PrivateMounts = true;
diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix
index 7c03c188356ef..f6bae8f9e9652 100644
--- a/nixos/modules/services/monitoring/prometheus/default.nix
+++ b/nixos/modules/services/monitoring/prometheus/default.nix
@@ -1563,13 +1563,7 @@ in
 
   options.services.prometheus = {
 
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Enable the Prometheus monitoring daemon.
-      '';
-    };
+    enable = mkEnableOption (lib.mdDoc "Prometheus monitoring daemon");
 
     package = mkOption {
       type = types.package;
@@ -1796,6 +1790,33 @@ in
         WorkingDirectory = workingDir;
         StateDirectory = cfg.stateDir;
         StateDirectoryMode = "0700";
+        # Hardening
+        AmbientCapabilities = lib.mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = if (cfg.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ];
+        DeviceAllow = [ "/dev/null rw" ];
+        DevicePolicy = "strict";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "full";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
       };
     };
     # prometheus-config-reload will activate after prometheus. However, what we
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index d9e380d42749b..8826d80a70c74 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -77,6 +77,7 @@ let
     "varnish"
     "wireguard"
     "flow"
+    "zfs"
   ] (name:
     import (./. + "/exporters/${name}.nix") { inherit config lib pkgs options; }
   );
@@ -196,7 +197,7 @@ let
         serviceConfig.LockPersonality = true;
         serviceConfig.MemoryDenyWriteExecute = true;
         serviceConfig.NoNewPrivileges = true;
-        serviceConfig.PrivateDevices = true;
+        serviceConfig.PrivateDevices = mkDefault true;
         serviceConfig.ProtectClock = mkDefault true;
         serviceConfig.ProtectControlGroups = true;
         serviceConfig.ProtectHome = true;
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/kea.nix b/nixos/modules/services/monitoring/prometheus/exporters/kea.nix
index 0682f9da4003a..ed33c72f644f3 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/kea.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/kea.nix
@@ -35,7 +35,7 @@ in {
         ${pkgs.prometheus-kea-exporter}/bin/kea-exporter \
           --address ${cfg.listenAddress} \
           --port ${toString cfg.port} \
-          ${concatStringsSep " \\n" cfg.controlSocketPaths}
+          ${concatStringsSep " " cfg.controlSocketPaths}
       '';
       SupplementaryGroups = [ "kea" ];
       RestrictAddressFamilies = [
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/mail.nix b/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
index 129c73eba4adf..15079f5841f43 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
@@ -33,7 +33,7 @@ let
       '';
     };
     port = mkOption {
-      type = types.int;
+      type = types.port;
       example = 587;
       description = lib.mdDoc ''
         Port to use for SMTP.
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/node.nix b/nixos/modules/services/monitoring/prometheus/exporters/node.nix
index ae69c29d0a51e..dd8602e2c63db 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/node.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/node.nix
@@ -4,6 +4,8 @@ with lib;
 
 let
   cfg = config.services.prometheus.exporters.node;
+  collectorIsEnabled = final: any (collector: (final == collector)) cfg.enabledCollectors;
+  collectorIsDisabled = final: any (collector: (final == collector)) cfg.disabledCollectors;
 in
 {
   port = 9100;
@@ -35,15 +37,15 @@ in
           ${concatMapStringsSep " " (x: "--no-collector." + x) cfg.disabledCollectors} \
           --web.listen-address ${cfg.listenAddress}:${toString cfg.port} ${concatStringsSep " " cfg.extraFlags}
       '';
-      RestrictAddressFamilies = optionals (any (collector: (collector == "logind" || collector == "systemd")) cfg.enabledCollectors) [
+      RestrictAddressFamilies = optionals (collectorIsEnabled "logind" || collectorIsEnabled "systemd") [
         # needs access to dbus via unix sockets (logind/systemd)
         "AF_UNIX"
-      ] ++ optionals (any (collector: (collector == "network_route" || collector == "wifi")) cfg.enabledCollectors) [
+      ] ++ optionals (collectorIsEnabled "network_route" || collectorIsEnabled "wifi" || ! collectorIsDisabled "netdev") [
         # needs netlink sockets for wireless collector
         "AF_NETLINK"
       ];
       # The timex collector needs to access clock APIs
-      ProtectClock = any (collector: collector == "timex") cfg.disabledCollectors;
+      ProtectClock = collectorIsDisabled "timex";
       # Allow space monitoring under /home
       ProtectHome = true;
     };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
index 8906c25d50376..df424ede6066b 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
@@ -50,7 +50,7 @@ in {
         "CAP_SYS_ADMIN"
       ];
       DevicePolicy = "closed";
-      DeviceAllow = lib.mkOverride 100 (
+      DeviceAllow = lib.mkOverride 50 (
         if cfg.devices != [] then
           cfg.devices
         else [
@@ -66,10 +66,7 @@ in {
       ProtectProc = "invisible";
       ProcSubset = "pid";
       SupplementaryGroups = [ "disk" ];
-      SystemCallFilter = [
-        "@system-service"
-        "~@privileged @resources"
-      ];
+      SystemCallFilter = [ "@system-service" "~@privileged" ];
     };
   };
 }
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/tor.nix b/nixos/modules/services/monitoring/prometheus/exporters/tor.nix
index edf9b57607a3c..7a9167110a279 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/tor.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/tor.nix
@@ -17,7 +17,7 @@ in
     };
 
     torControlPort = mkOption {
-      type = types.int;
+      type = types.port;
       default = 9051;
       description = lib.mdDoc ''
         Tor control port.
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix b/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix
index d264e68be9af7..d1c82b2fd1c22 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix
@@ -13,6 +13,7 @@ let
       http_listen = "${cfg.listenAddress}:${toString cfg.port}";
       report_errors = cfg.log.prometheusErrors;
     };
+    inherit (cfg) loki;
   });
 
 in {
@@ -20,6 +21,7 @@ in {
 
   extraOpts = {
     inherit (options.services.unifi-poller.unifi) controllers;
+    inherit (options.services.unifi-poller) loki;
     log = {
       debug = mkEnableOption (lib.mdDoc "debug logging including line numbers, high resolution timestamps, per-device logs.");
       quiet = mkEnableOption (lib.mdDoc "startup and error logs only.");
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/zfs.nix b/nixos/modules/services/monitoring/prometheus/exporters/zfs.nix
new file mode 100644
index 0000000000000..ff12a52d49a92
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/zfs.nix
@@ -0,0 +1,44 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.zfs;
+in
+{
+  port = 9134;
+
+  extraOpts = {
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = lib.mdDoc ''
+        Path under which to expose metrics.
+      '';
+    };
+
+    pools = mkOption {
+      type = with types; nullOr (listOf str);
+      default = [ ];
+      description = lib.mdDoc ''
+        Name of the pool(s) to collect, repeat for multiple pools (default: all pools).
+      '';
+    };
+  };
+
+  serviceOpts = {
+    # needs zpool
+    path = [ config.boot.zfs.package ];
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-zfs-exporter}/bin/zfs_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path ${cfg.telemetryPath} \
+          ${concatMapStringsSep " " (x: "--pool=${x}") cfg.pools} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      ProtectClock = false;
+      PrivateDevices = false;
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/riemann.nix b/nixos/modules/services/monitoring/riemann.nix
index 8d61ec2a308ff..7ab8af85ed79b 100644
--- a/nixos/modules/services/monitoring/riemann.nix
+++ b/nixos/modules/services/monitoring/riemann.nix
@@ -27,13 +27,8 @@ in {
   options = {
 
     services.riemann = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Enable the Riemann network monitoring daemon.
-        '';
-      };
+      enable = mkEnableOption (lib.mdDoc "Riemann network monitoring daemon");
+
       config = mkOption {
         type = types.lines;
         description = lib.mdDoc ''
diff --git a/nixos/modules/services/monitoring/smartd.nix b/nixos/modules/services/monitoring/smartd.nix
index cfebb4b9798bd..1e654cad5dd2a 100644
--- a/nixos/modules/services/monitoring/smartd.nix
+++ b/nixos/modules/services/monitoring/smartd.nix
@@ -4,8 +4,7 @@ with lib;
 
 let
 
-  host = config.networking.hostName or "unknown"
-       + optionalString (config.networking.domain != null) ".${config.networking.domain}";
+  host = config.networking.fqdnOrHostName;
 
   cfg = config.services.smartd;
   opt = options.services.smartd;
diff --git a/nixos/modules/services/monitoring/teamviewer.nix b/nixos/modules/services/monitoring/teamviewer.nix
index f77b194c5656d..9b1278317943d 100644
--- a/nixos/modules/services/monitoring/teamviewer.nix
+++ b/nixos/modules/services/monitoring/teamviewer.nix
@@ -30,7 +30,7 @@ in
       description = "TeamViewer remote control daemon";
 
       wantedBy = [ "multi-user.target" ];
-      after = [ "NetworkManager-wait-online.service" "network.target" "dbus.service" ];
+      after = [ "network-online.target" "network.target" "dbus.service" ];
       requires = [ "dbus.service" ];
       preStart = "mkdir -pv /var/lib/teamviewer /var/log/teamviewer";
 
diff --git a/nixos/modules/services/monitoring/uptime-kuma.nix b/nixos/modules/services/monitoring/uptime-kuma.nix
new file mode 100644
index 0000000000000..3a6091de679d9
--- /dev/null
+++ b/nixos/modules/services/monitoring/uptime-kuma.nix
@@ -0,0 +1,76 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.uptime-kuma;
+in
+{
+
+  options = {
+    services.uptime-kuma = {
+      enable = mkEnableOption (mdDoc "Uptime Kuma, this assumes a reverse proxy to be set.");
+
+      package = mkOption {
+        type = types.package;
+        example = literalExpression "pkgs.uptime-kuma";
+        default = pkgs.uptime-kuma;
+        defaultText = "pkgs.uptime-kuma";
+        description = lib.mdDoc "Uptime Kuma package to use.";
+      };
+
+      settings = lib.mkOption {
+        type =
+          lib.types.submodule { freeformType = with lib.types; attrsOf str; };
+        default = { };
+        example = {
+          PORT = "4000";
+          NODE_EXTRA_CA_CERTS = "/etc/ssl/certs/ca-certificates.crt";
+        };
+        description = lib.mdDoc ''
+          Additional configuration for Uptime Kuma, see
+          <https://github.com/louislam/uptime-kuma/wiki/Environment-Variables">
+          for supported values.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.uptime-kuma.settings = {
+      DATA_DIR = "/var/lib/uptime-kuma/";
+      NODE_ENV = mkDefault "production";
+    };
+
+    systemd.services.uptime-kuma = {
+      description = "Uptime Kuma";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = cfg.settings;
+      serviceConfig = {
+        Type = "simple";
+        StateDirectory = "uptime-kuma";
+        DynamicUser = true;
+        ExecStart = "${cfg.package}/bin/uptime-kuma-server";
+        Restart = "on-failure";
+        ProtectHome = true;
+        ProtectSystem = "strict";
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        NoNewPrivileges = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+        PrivateMounts = true;
+      };
+    };
+  };
+}
+
diff --git a/nixos/modules/services/monitoring/vmagent.nix b/nixos/modules/services/monitoring/vmagent.nix
new file mode 100644
index 0000000000000..c793bb073199c
--- /dev/null
+++ b/nixos/modules/services/monitoring/vmagent.nix
@@ -0,0 +1,100 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.services.vmagent;
+  settingsFormat = pkgs.formats.json { };
+in {
+  options.services.vmagent = {
+    enable = mkEnableOption (lib.mdDoc "vmagent");
+
+    user = mkOption {
+      default = "vmagent";
+      type = types.str;
+      description = lib.mdDoc ''
+        User account under which vmagent runs.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "vmagent";
+      description = lib.mdDoc ''
+        Group under which vmagent runs.
+      '';
+    };
+
+    package = mkOption {
+      default = pkgs.vmagent;
+      defaultText = lib.literalMD "pkgs.vmagent";
+      type = types.package;
+      description = lib.mdDoc ''
+        vmagent package to use.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/vmagent";
+      description = lib.mdDoc ''
+        The directory where vmagent stores its data files.
+      '';
+    };
+
+    remoteWriteUrl = mkOption {
+      default = "http://localhost:8428/api/v1/write";
+      type = types.str;
+      description = lib.mdDoc ''
+        The storage endpoint such as VictoriaMetrics
+      '';
+    };
+
+    prometheusConfig = mkOption {
+      type = lib.types.submodule { freeformType = settingsFormat.type; };
+      description = lib.mdDoc ''
+        Config for prometheus style metrics
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Whether to open the firewall for the default ports.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups = mkIf (cfg.group == "vmagent") { vmagent = { }; };
+
+    users.users = mkIf (cfg.user == "vmagent") {
+      vmagent = {
+        group = cfg.group;
+        description = "vmagent daemon user";
+        home = cfg.dataDir;
+        isSystemUser = true;
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ 8429 ];
+
+    systemd.services.vmagent = let
+      prometheusConfig = settingsFormat.generate "prometheusConfig.yaml" cfg.prometheusConfig;
+    in {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      description = "vmagent system service";
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        Type = "simple";
+        Restart = "on-failure";
+        WorkingDirectory = cfg.dataDir;
+        ExecStart = "${cfg.package}/bin/vmagent -remoteWrite.url=${cfg.remoteWriteUrl} -promscrape.config=${prometheusConfig}";
+      };
+    };
+
+    systemd.tmpfiles.rules =
+      [ "d '${cfg.dataDir}' 0755 ${cfg.user} ${cfg.group} -" ];
+  };
+}
diff --git a/nixos/modules/services/monitoring/zabbix-server.nix b/nixos/modules/services/monitoring/zabbix-server.nix
index b40437100fc8a..2b50280e3969a 100644
--- a/nixos/modules/services/monitoring/zabbix-server.nix
+++ b/nixos/modules/services/monitoring/zabbix-server.nix
@@ -94,7 +94,7 @@ in
         };
 
         port = mkOption {
-          type = types.int;
+          type = types.port;
           default = if cfg.database.type == "mysql" then mysql.port else pgsql.port;
           defaultText = literalExpression ''
             if config.${opt.database.type} == "mysql"
diff --git a/nixos/modules/services/network-filesystems/kubo.nix b/nixos/modules/services/network-filesystems/kubo.nix
index 9942e4e41ad64..51e1282db4189 100644
--- a/nixos/modules/services/network-filesystems/kubo.nix
+++ b/nixos/modules/services/network-filesystems/kubo.nix
@@ -3,6 +3,8 @@ with lib;
 let
   cfg = config.services.kubo;
 
+  settingsFormat = pkgs.formats.json {};
+
   kuboFlags = utils.escapeSystemdExecArgs (
     optional cfg.autoMount "--mount" ++
     optional cfg.enableGC "--enable-gc" ++
@@ -117,29 +119,6 @@ in
         description = lib.mdDoc "Where to mount the IPNS namespace to";
       };
 
-      gatewayAddress = mkOption {
-        type = types.str;
-        default = "/ip4/127.0.0.1/tcp/8080";
-        description = lib.mdDoc "Where the IPFS Gateway can be reached";
-      };
-
-      apiAddress = mkOption {
-        type = types.str;
-        default = "/ip4/127.0.0.1/tcp/5001";
-        description = lib.mdDoc "Where Kubo exposes its API to";
-      };
-
-      swarmAddress = mkOption {
-        type = types.listOf types.str;
-        default = [
-          "/ip4/0.0.0.0/tcp/4001"
-          "/ip6/::/tcp/4001"
-          "/ip4/0.0.0.0/udp/4001/quic"
-          "/ip6/::/udp/4001/quic"
-        ];
-        description = lib.mdDoc "Where Kubo listens for incoming p2p connections";
-      };
-
       enableGC = mkOption {
         type = types.bool;
         default = false;
@@ -152,11 +131,38 @@ in
         description = lib.mdDoc "If set to true, the repo won't be initialized with help files";
       };
 
-      extraConfig = mkOption {
-        type = types.attrs;
+      settings = mkOption {
+        type = lib.types.submodule {
+          freeformType = settingsFormat.type;
+
+          options = {
+            Addresses.API = mkOption {
+              type = types.str;
+              default = "/ip4/127.0.0.1/tcp/5001";
+              description = lib.mdDoc "Where Kubo exposes its API to";
+            };
+
+            Addresses.Gateway = mkOption {
+              type = types.str;
+              default = "/ip4/127.0.0.1/tcp/8080";
+              description = lib.mdDoc "Where the IPFS Gateway can be reached";
+            };
+
+            Addresses.Swarm = mkOption {
+              type = types.listOf types.str;
+              default = [
+                "/ip4/0.0.0.0/tcp/4001"
+                "/ip6/::/tcp/4001"
+                "/ip4/0.0.0.0/udp/4001/quic"
+                "/ip6/::/udp/4001/quic"
+              ];
+              description = lib.mdDoc "Where Kubo listens for incoming p2p connections";
+            };
+          };
+        };
         description = lib.mdDoc ''
           Attrset of daemon configuration to set using {command}`ipfs config`, every time the daemon starts.
-          These are applied last, so may override configuration set by other options in this module.
+          See [https://github.com/ipfs/kubo/blob/master/docs/config.md](https://github.com/ipfs/kubo/blob/master/docs/config.md) for reference.
           Keep in mind that this configuration is stateful; i.e., unsetting anything in here does not reset the value to the default!
         '';
         default = { };
@@ -244,6 +250,12 @@ in
       then [ cfg.package.systemd_unit ]
       else [ cfg.package.systemd_unit_hardened ];
 
+    services.kubo.settings = mkIf cfg.autoMount {
+      Mounts.FuseAllowOther = lib.mkDefault true;
+      Mounts.IPFS = lib.mkDefault cfg.ipfsMountDir;
+      Mounts.IPNS = lib.mkDefault cfg.ipnsMountDir;
+    };
+
     systemd.services.ipfs = {
       path = [ "/run/wrappers" cfg.package ];
       environment.IPFS_PATH = cfg.dataDir;
@@ -259,22 +271,10 @@ in
       '' + ''
           ipfs --offline config profile apply ${profile} >/dev/null
         fi
-      '' + optionalString cfg.autoMount ''
-        ipfs --offline config Mounts.FuseAllowOther --json true
-        ipfs --offline config Mounts.IPFS ${cfg.ipfsMountDir}
-        ipfs --offline config Mounts.IPNS ${cfg.ipnsMountDir}
       '' + ''
         ipfs --offline config show \
-          | ${pkgs.jq}/bin/jq '. * $extraConfig' --argjson extraConfig ${
-              escapeShellArg (builtins.toJSON (
-                recursiveUpdate
-                  {
-                    Addresses.API = cfg.apiAddress;
-                    Addresses.Gateway = cfg.gatewayAddress;
-                    Addresses.Swarm = cfg.swarmAddress;
-                  }
-                  cfg.extraConfig
-              ))
+          | ${pkgs.jq}/bin/jq '. * $settings' --argjson settings ${
+              escapeShellArg (builtins.toJSON cfg.settings)
             } \
           | ipfs --offline config replace -
       '';
@@ -294,12 +294,12 @@ in
       socketConfig = {
         ListenStream =
           let
-            fromCfg = multiaddrToListenStream cfg.gatewayAddress;
+            fromCfg = multiaddrToListenStream cfg.settings.Addresses.Gateway;
           in
           [ "" ] ++ lib.optional (fromCfg != null) fromCfg;
         ListenDatagram =
           let
-            fromCfg = multiaddrToListenDatagram cfg.gatewayAddress;
+            fromCfg = multiaddrToListenDatagram cfg.settings.Addresses.Gateway;
           in
           [ "" ] ++ lib.optional (fromCfg != null) fromCfg;
       };
@@ -311,7 +311,7 @@ in
       # in the multiaddr.
       socketConfig.ListenStream =
         let
-          fromCfg = multiaddrToListenStream cfg.apiAddress;
+          fromCfg = multiaddrToListenStream cfg.settings.Addresses.API;
         in
         [ "" "%t/ipfs.sock" ] ++ lib.optional (fromCfg != null) fromCfg;
     };
@@ -332,15 +332,19 @@ in
     (mkRenamedOptionModule [ "services" "ipfs" "autoMigrate" ] [ "services" "kubo" "autoMigrate" ])
     (mkRenamedOptionModule [ "services" "ipfs" "ipfsMountDir" ] [ "services" "kubo" "ipfsMountDir" ])
     (mkRenamedOptionModule [ "services" "ipfs" "ipnsMountDir" ] [ "services" "kubo" "ipnsMountDir" ])
-    (mkRenamedOptionModule [ "services" "ipfs" "gatewayAddress" ] [ "services" "kubo" "gatewayAddress" ])
-    (mkRenamedOptionModule [ "services" "ipfs" "apiAddress" ] [ "services" "kubo" "apiAddress" ])
-    (mkRenamedOptionModule [ "services" "ipfs" "swarmAddress" ] [ "services" "kubo" "swarmAddress" ])
+    (mkRenamedOptionModule [ "services" "ipfs" "gatewayAddress" ] [ "services" "kubo" "settings" "Addresses" "Gateway" ])
+    (mkRenamedOptionModule [ "services" "ipfs" "apiAddress" ] [ "services" "kubo" "settings" "Addresses" "API" ])
+    (mkRenamedOptionModule [ "services" "ipfs" "swarmAddress" ] [ "services" "kubo" "settings" "Addresses" "Swarm" ])
     (mkRenamedOptionModule [ "services" "ipfs" "enableGC" ] [ "services" "kubo" "enableGC" ])
     (mkRenamedOptionModule [ "services" "ipfs" "emptyRepo" ] [ "services" "kubo" "emptyRepo" ])
-    (mkRenamedOptionModule [ "services" "ipfs" "extraConfig" ] [ "services" "kubo" "extraConfig" ])
+    (mkRenamedOptionModule [ "services" "ipfs" "extraConfig" ] [ "services" "kubo" "settings" ])
     (mkRenamedOptionModule [ "services" "ipfs" "extraFlags" ] [ "services" "kubo" "extraFlags" ])
     (mkRenamedOptionModule [ "services" "ipfs" "localDiscovery" ] [ "services" "kubo" "localDiscovery" ])
     (mkRenamedOptionModule [ "services" "ipfs" "serviceFdlimit" ] [ "services" "kubo" "serviceFdlimit" ])
     (mkRenamedOptionModule [ "services" "ipfs" "startWhenNeeded" ] [ "services" "kubo" "startWhenNeeded" ])
+    (mkRenamedOptionModule [ "services" "kubo" "extraConfig" ] [ "services" "kubo" "settings" ])
+    (mkRenamedOptionModule [ "services" "kubo" "gatewayAddress" ] [ "services" "kubo" "settings" "Addresses" "Gateway" ])
+    (mkRenamedOptionModule [ "services" "kubo" "apiAddress" ] [ "services" "kubo" "settings" "Addresses" "API" ])
+    (mkRenamedOptionModule [ "services" "kubo" "swarmAddress" ] [ "services" "kubo" "settings" "Addresses" "Swarm" ])
   ];
 }
diff --git a/nixos/modules/services/network-filesystems/litestream/litestream.xml b/nixos/modules/services/network-filesystems/litestream/litestream.xml
index 598f9be8cf632..8f5597bb6891e 100644
--- a/nixos/modules/services/network-filesystems/litestream/litestream.xml
+++ b/nixos/modules/services/network-filesystems/litestream/litestream.xml
@@ -15,7 +15,7 @@
   <para>
    Litestream service is managed by a dedicated user named <literal>litestream</literal>
    which needs permission to the database file. Here's an example config which gives
-   required permissions to access <link linkend="opt-services.grafana.database.path">
+   required permissions to access <link linkend="opt-services.grafana.settings.database.path">
    grafana database</link>:
 <programlisting>
 { pkgs, ... }:
diff --git a/nixos/modules/services/network-filesystems/samba-wsdd.nix b/nixos/modules/services/network-filesystems/samba-wsdd.nix
index e28fe4cf9c4d2..24407f05de6af 100644
--- a/nixos/modules/services/network-filesystems/samba-wsdd.nix
+++ b/nixos/modules/services/network-filesystems/samba-wsdd.nix
@@ -9,7 +9,7 @@ in {
   options = {
     services.samba-wsdd = {
       enable = mkEnableOption (lib.mdDoc ''
-        Enable Web Services Dynamic Discovery host daemon. This enables (Samba) hosts, like your local NAS device,
+        Web Services Dynamic Discovery host daemon. This enables (Samba) hosts, like your local NAS device,
         to be found by Web Service Discovery Clients like Windows.
 
         ::: {.note}
diff --git a/nixos/modules/services/networking/adguardhome.nix b/nixos/modules/services/networking/adguardhome.nix
index 13b6f6efcd6d1..bda99cb7942b8 100644
--- a/nixos/modules/services/networking/adguardhome.nix
+++ b/nixos/modules/services/networking/adguardhome.nix
@@ -12,36 +12,25 @@ let
     "--config /var/lib/AdGuardHome/AdGuardHome.yaml"
   ] ++ cfg.extraArgs);
 
-  baseConfig = {
-    bind_host = cfg.host;
-    bind_port = cfg.port;
-  };
-
   configFile = pkgs.writeTextFile {
     name = "AdGuardHome.yaml";
-    text = builtins.toJSON (recursiveUpdate cfg.settings baseConfig);
+    text = builtins.toJSON cfg.settings;
     checkPhase = "${pkgs.adguardhome}/bin/adguardhome -c $out --check-config";
   };
 
-in {
-  options.services.adguardhome = with types; {
-    enable = mkEnableOption (lib.mdDoc "AdGuard Home network-wide ad blocker");
+in
+{
 
-    host = mkOption {
-      default = "0.0.0.0";
-      type = str;
-      description = lib.mdDoc ''
-        Host address to bind HTTP server to.
-      '';
-    };
+  imports =
+    let cfgPath = [ "services" "adguardhome" ];
+    in
+    [
+      (mkRenamedOptionModuleWith { sinceRelease = 2211; from = cfgPath ++ [ "host" ]; to = cfgPath ++ [ "settings" "bind_host" ]; })
+      (mkRenamedOptionModuleWith { sinceRelease = 2211; from = cfgPath ++ [ "port" ]; to = cfgPath ++ [ "settings" "bind_port" ]; })
+    ];
 
-    port = mkOption {
-      default = 3000;
-      type = port;
-      description = lib.mdDoc ''
-        Port to serve HTTP pages on.
-      '';
-    };
+  options.services.adguardhome = with types; {
+    enable = mkEnableOption (lib.mdDoc "AdGuard Home network-wide ad blocker");
 
     openFirewall = mkOption {
       default = false;
@@ -62,8 +51,35 @@ in {
     };
 
     settings = mkOption {
-      type = (pkgs.formats.yaml { }).type;
-      default = { };
+      default = null;
+      type = nullOr (submodule {
+        freeformType = (pkgs.formats.yaml { }).type;
+        options = {
+          schema_version = mkOption {
+            default = pkgs.adguardhome.schema_version;
+            defaultText = literalExpression "pkgs.adguardhome.schema_version";
+            type = int;
+            description = lib.mdDoc ''
+              Schema version for the configuration.
+              Defaults to the `schema_version` supplied by `pkgs.adguardhome`.
+            '';
+          };
+          bind_host = mkOption {
+            default = "0.0.0.0";
+            type = str;
+            description = lib.mdDoc ''
+              Host address to bind HTTP server to.
+            '';
+          };
+          bind_port = mkOption {
+            default = 3000;
+            type = port;
+            description = lib.mdDoc ''
+              Port to serve HTTP pages on.
+            '';
+          };
+        };
+      });
       description = lib.mdDoc ''
         AdGuard Home configuration. Refer to
         <https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#configuration-file>
@@ -73,6 +89,10 @@ in {
         On start and if {option}`mutableSettings` is `true`,
         these options are merged into the configuration file on start, taking
         precedence over configuration changes made on the web interface.
+
+        Set this to `null` (default) for a non-declarative configuration without any
+        Nix-supplied values.
+        Declarative configurations are supplied with a default `schema_version`, `bind_host`, and `bind_port`.
         :::
       '';
     };
@@ -89,15 +109,15 @@ in {
   config = mkIf cfg.enable {
     assertions = [
       {
-        assertion = cfg.settings != { }
-          -> (hasAttrByPath [ "dns" "bind_host" ] cfg.settings)
+        assertion = cfg.settings != null -> cfg.mutableSettings
+          || (hasAttrByPath [ "dns" "bind_host" ] cfg.settings)
           || (hasAttrByPath [ "dns" "bind_hosts" ] cfg.settings);
         message =
           "AdGuard setting dns.bind_host or dns.bind_hosts needs to be configured for a minimal working configuration";
       }
       {
-        assertion = cfg.settings != { }
-          -> hasAttrByPath [ "dns" "bootstrap_dns" ] cfg.settings;
+        assertion = cfg.settings != null -> cfg.mutableSettings
+          || hasAttrByPath [ "dns" "bootstrap_dns" ] cfg.settings;
         message =
           "AdGuard setting dns.bootstrap_dns needs to be configured for a minimal working configuration";
       }
@@ -112,7 +132,7 @@ in {
         StartLimitBurst = 10;
       };
 
-      preStart = optionalString (cfg.settings != { }) ''
+      preStart = optionalString (cfg.settings != null) ''
         if    [ -e "$STATE_DIRECTORY/AdGuardHome.yaml" ] \
            && [ "${toString cfg.mutableSettings}" = "1" ]; then
           # Writing directly to AdGuardHome.yaml results in empty file
@@ -135,6 +155,6 @@ in {
       };
     };
 
-    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.bind_port ];
   };
 }
diff --git a/nixos/modules/services/networking/bitcoind.nix b/nixos/modules/services/networking/bitcoind.nix
index e8b0fb65ffcf8..6df809a8b7648 100644
--- a/nixos/modules/services/networking/bitcoind.nix
+++ b/nixos/modules/services/networking/bitcoind.nix
@@ -204,7 +204,7 @@ in
         '';
       in {
         description = "Bitcoin daemon";
-        after = [ "network.target" ];
+        after = [ "network-online.target" ];
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
           User = cfg.user;
diff --git a/nixos/modules/services/networking/bitlbee.nix b/nixos/modules/services/networking/bitlbee.nix
index 668ed388ae2c3..88c04597e2bc6 100644
--- a/nixos/modules/services/networking/bitlbee.nix
+++ b/nixos/modules/services/networking/bitlbee.nix
@@ -68,7 +68,7 @@ in
 
       portNumber = mkOption {
         default = 6667;
-        type = types.int;
+        type = types.port;
         description = lib.mdDoc ''
           Number of the port BitlBee will be listening to.
         '';
diff --git a/nixos/modules/services/networking/blocky.nix b/nixos/modules/services/networking/blocky.nix
index 2acbcea2aa41d..9714485456161 100644
--- a/nixos/modules/services/networking/blocky.nix
+++ b/nixos/modules/services/networking/blocky.nix
@@ -10,7 +10,7 @@ let
 in
 {
   options.services.blocky = {
-    enable = mkEnableOption (lib.mdDoc "Fast and lightweight DNS proxy as ad-blocker for local network with many features");
+    enable = mkEnableOption (lib.mdDoc "blocky, a fast and lightweight DNS proxy as ad-blocker for local network with many features");
 
     settings = mkOption {
       type = format.type;
diff --git a/nixos/modules/services/networking/chisel-server.nix b/nixos/modules/services/networking/chisel-server.nix
new file mode 100644
index 0000000000000..d3724743209b2
--- /dev/null
+++ b/nixos/modules/services/networking/chisel-server.nix
@@ -0,0 +1,99 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.chisel-server;
+
+in {
+  options = {
+    services.chisel-server = {
+      enable = mkEnableOption (mdDoc "Chisel Tunnel Server");
+      host = mkOption {
+        description = mdDoc "Address to listen on, falls back to 0.0.0.0";
+        type = with types; nullOr str;
+        default = null;
+        example = "[::1]";
+      };
+      port = mkOption {
+        description = mdDoc "Port to listen on, falls back to 8080";
+        type = with types; nullOr int;
+        default = null;
+      };
+      authfile = mkOption {
+        description = mdDoc "Path to auth.json file";
+        type = with types; nullOr path;
+        default = null;
+      };
+      keepalive  = mkOption {
+        description = mdDoc "Keepalive interval, falls back to 25s";
+        type = with types; nullOr str;
+        default = null;
+        example = "5s";
+      };
+      backend = mkOption {
+        description = mdDoc "HTTP server to proxy normal requests to";
+        type = with types; nullOr str;
+        default = null;
+        example = "http://127.0.0.1:8888";
+      };
+      socks5 = mkOption {
+        description = mdDoc "Allow clients access to internal SOCKS5 proxy";
+        type = types.bool;
+        default = false;
+      };
+      reverse = mkOption {
+        description = mdDoc "Allow clients reverse port forwarding";
+        type = types.bool;
+        default = false;
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.chisel-server = {
+      description = "Chisel Tunnel Server";
+      wantedBy = [ "network-online.target" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.chisel}/bin/chisel server " + concatStringsSep " " (
+          optional (cfg.host != null) "--host ${cfg.host}"
+          ++ optional (cfg.port != null) "--port ${builtins.toString cfg.port}"
+          ++ optional (cfg.authfile != null) "--authfile ${cfg.authfile}"
+          ++ optional (cfg.keepalive != null) "--keepalive ${cfg.keepalive}"
+          ++ optional (cfg.backend != null) "--backend ${cfg.backend}"
+          ++ optional cfg.socks5 "--socks5"
+          ++ optional cfg.reverse "--reverse"
+        );
+
+        # Security Hardening
+        # Refer to systemd.exec(5) for option descriptions.
+        CapabilityBoundingSet = "";
+
+        # implies RemoveIPC=, PrivateTmp=, NoNewPrivileges=, RestrictSUIDSGID=,
+        # ProtectSystem=strict, ProtectHome=read-only
+        DynamicUser = true;
+        LockPersonality = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectProc = "invisible";
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@clock @cpu-emulation @debug @mount @obsolete @reboot @swap @privileged @resources";
+        UMask = "0077";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ clerie ];
+}
diff --git a/nixos/modules/services/networking/consul.nix b/nixos/modules/services/networking/consul.nix
index 9f3c081e73516..bee41dcf765d2 100644
--- a/nixos/modules/services/networking/consul.nix
+++ b/nixos/modules/services/networking/consul.nix
@@ -201,7 +201,7 @@ in
         serviceConfig = {
           ExecStart = "@${cfg.package}/bin/consul consul agent -config-dir /etc/consul.d"
             + concatMapStrings (n: " -config-file ${n}") configFiles;
-          ExecReload = "${cfg.package}/bin/consul reload";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
           PermissionsStartOnly = true;
           User = if cfg.dropPrivileges then "consul" else null;
           Restart = "on-failure";
diff --git a/nixos/modules/services/networking/croc.nix b/nixos/modules/services/networking/croc.nix
index d3902611a625a..45bfd447da454 100644
--- a/nixos/modules/services/networking/croc.nix
+++ b/nixos/modules/services/networking/croc.nix
@@ -72,7 +72,7 @@ in
         RuntimeDirectoryMode = "700";
         SystemCallFilter = [
           "@system-service"
-          "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@setuid" "~@sync" "~@timer"
+          "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@setuid" "~@sync" "~@timer"
         ];
         SystemCallArchitectures = "native";
         SystemCallErrorNumber = "EPERM";
diff --git a/nixos/modules/services/networking/dnscrypt-proxy2.nix b/nixos/modules/services/networking/dnscrypt-proxy2.nix
index 99ff5ee0bd8f9..de1ca0d2f2061 100644
--- a/nixos/modules/services/networking/dnscrypt-proxy2.nix
+++ b/nixos/modules/services/networking/dnscrypt-proxy2.nix
@@ -111,7 +111,6 @@ in
           "~@aio"
           "~@keyring"
           "~@memlock"
-          "~@resources"
           "~@setuid"
           "~@timer"
         ];
diff --git a/nixos/modules/services/networking/dnscrypt-wrapper.nix b/nixos/modules/services/networking/dnscrypt-wrapper.nix
index 06b7ea24e2d9c..082e0195093ef 100644
--- a/nixos/modules/services/networking/dnscrypt-wrapper.nix
+++ b/nixos/modules/services/networking/dnscrypt-wrapper.nix
@@ -135,7 +135,7 @@ in {
     };
 
     port = mkOption {
-      type = types.int;
+      type = types.port;
       default = 5353;
       description = lib.mdDoc ''
         The DNSCrypt wrapper will listen for DNS queries on this port.
@@ -182,7 +182,7 @@ in {
     };
 
     upstream.port = mkOption {
-      type = types.int;
+      type = types.port;
       default = 53;
       description = lib.mdDoc ''
         The port of the upstream DNS server DNSCrypt will "wrap".
diff --git a/nixos/modules/services/networking/eternal-terminal.nix b/nixos/modules/services/networking/eternal-terminal.nix
index 555307459e396..c6b6b04dcf72e 100644
--- a/nixos/modules/services/networking/eternal-terminal.nix
+++ b/nixos/modules/services/networking/eternal-terminal.nix
@@ -20,7 +20,7 @@ in
 
       port = mkOption {
         default = 2022;
-        type = types.int;
+        type = types.port;
         description = lib.mdDoc ''
           The port the server should listen on. Will use the server's default (2022) if not specified.
 
diff --git a/nixos/modules/services/networking/firefox-syncserver.nix b/nixos/modules/services/networking/firefox-syncserver.nix
index d7d5df59a4eb3..c3d9f43f74577 100644
--- a/nixos/modules/services/networking/firefox-syncserver.nix
+++ b/nixos/modules/services/networking/firefox-syncserver.nix
@@ -32,6 +32,44 @@ let
     };
   };
   configFile = format.generate "syncstorage.toml" (lib.recursiveUpdate settings cfg.settings);
+  setupScript = pkgs.writeShellScript "firefox-syncserver-setup" ''
+        set -euo pipefail
+        shopt -s inherit_errexit
+
+        schema_configured() {
+          mysql ${cfg.database.name} -Ne 'SHOW TABLES' | grep -q services
+        }
+
+        update_config() {
+          mysql ${cfg.database.name} <<"EOF"
+            BEGIN;
+
+            INSERT INTO `services` (`id`, `service`, `pattern`)
+              VALUES (1, 'sync-1.5', '{node}/1.5/{uid}')
+              ON DUPLICATE KEY UPDATE service='sync-1.5', pattern='{node}/1.5/{uid}';
+            INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`,
+                                 `capacity`, `downed`, `backoff`)
+              VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity},
+              0, ${toString cfg.singleNode.capacity}, 0, 0)
+              ON DUPLICATE KEY UPDATE node = '${cfg.singleNode.url}', capacity=${toString cfg.singleNode.capacity};
+
+            COMMIT;
+        EOF
+        }
+
+
+        for (( try = 0; try < 60; try++ )); do
+          if ! schema_configured; then
+            sleep 2
+          else
+            update_config
+            exit 0
+          fi
+        done
+
+        echo "Single-node setup failed"
+        exit 1
+      '';
 in
 
 {
@@ -210,6 +248,7 @@ in
       wantedBy = [ "multi-user.target" ];
       requires = lib.mkIf dbIsLocal [ "mysql.service" ];
       after = lib.mkIf dbIsLocal [ "mysql.service" ];
+      restartTriggers = lib.optional cfg.singleNode.enable setupScript;
       environment.RUST_LOG = cfg.logLevel;
       serviceConfig = {
         User = defaultUser;
@@ -255,56 +294,7 @@ in
       requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service";
       after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service";
       path = [ config.services.mysql.package ];
-      script = ''
-        set -euo pipefail
-        shopt -s inherit_errexit
-
-        schema_configured() {
-          mysql ${cfg.database.name} -Ne 'SHOW TABLES' | grep -q services
-        }
-
-        services_configured() {
-          [ 1 != $(mysql ${cfg.database.name} -Ne 'SELECT COUNT(*) < 1 FROM `services`') ]
-        }
-
-        create_services() {
-          mysql ${cfg.database.name} <<"EOF"
-            BEGIN;
-
-            INSERT INTO `services` (`id`, `service`, `pattern`)
-              VALUES (1, 'sync-1.5', '{node}/1.5/{uid}');
-            INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`,
-                                 `capacity`, `downed`, `backoff`)
-              VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity},
-                      0, ${toString cfg.singleNode.capacity}, 0, 0);
-
-            COMMIT;
-        EOF
-        }
-
-        update_nodes() {
-          mysql ${cfg.database.name} <<"EOF"
-            UPDATE `nodes`
-              SET `capacity` = ${toString cfg.singleNode.capacity}
-              WHERE `id` = 1;
-        EOF
-        }
-
-        for (( try = 0; try < 60; try++ )); do
-          if ! schema_configured; then
-            sleep 2
-          elif services_configured; then
-            update_nodes
-            exit 0
-          else
-            create_services
-            exit 0
-          fi
-        done
-
-        echo "Single-node setup failed"
-        exit 1
-      '';
+      serviceConfig.ExecStart = [ "${setupScript}" ];
     };
 
     services.nginx.virtualHosts = lib.mkIf cfg.singleNode.enableNginx {
diff --git a/nixos/modules/services/networking/flannel.nix b/nixos/modules/services/networking/flannel.nix
index c19e51e5aa6c8..6ed4f78ddc92b 100644
--- a/nixos/modules/services/networking/flannel.nix
+++ b/nixos/modules/services/networking/flannel.nix
@@ -92,10 +92,8 @@ in {
         Needed when running with Kubernetes as backend as this cannot be auto-detected";
       '';
       type = types.nullOr types.str;
-      default = with config.networking; (hostName + optionalString (domain != null) ".${domain}");
-      defaultText = literalExpression ''
-        with config.networking; (hostName + optionalString (domain != null) ".''${domain}")
-      '';
+      default = config.networking.fqdnOrHostName;
+      defaultText = literalExpression "config.networking.fqdnOrHostName";
       example = "node1.example.com";
     };
 
diff --git a/nixos/modules/services/networking/hostapd.nix b/nixos/modules/services/networking/hostapd.nix
index ec1a7a58b1e09..63bb44256dd69 100644
--- a/nixos/modules/services/networking/hostapd.nix
+++ b/nixos/modules/services/networking/hostapd.nix
@@ -199,7 +199,7 @@ in
 
     environment.systemPackages =  [ pkgs.hostapd ];
 
-    services.udev.packages = optional (cfg.countryCode != null) [ pkgs.crda ];
+    services.udev.packages = optionals (cfg.countryCode != null) [ pkgs.crda ];
 
     systemd.services.hostapd =
       { description = "hostapd wireless AP";
diff --git a/nixos/modules/services/networking/jibri/default.nix b/nixos/modules/services/networking/jibri/default.nix
index 6925ac55840ec..a931831fc2814 100644
--- a/nixos/modules/services/networking/jibri/default.nix
+++ b/nixos/modules/services/networking/jibri/default.nix
@@ -378,7 +378,7 @@ in
         '')
         cfg.xmppEnvironments))
       + ''
-        ${pkgs.jre8_headless}/bin/java -Djava.util.logging.config.file=${./logging.properties-journal} -Dconfig.file=${configFile} -jar ${pkgs.jibri}/opt/jitsi/jibri/jibri.jar --config /var/lib/jibri/jibri.json
+        ${pkgs.jdk11_headless}/bin/java -Djava.util.logging.config.file=${./logging.properties-journal} -Dconfig.file=${configFile} -jar ${pkgs.jibri}/opt/jitsi/jibri/jibri.jar --config /var/lib/jibri/jibri.json
       '';
 
       environment.HOME = "/var/lib/jibri";
diff --git a/nixos/modules/services/networking/jitsi-videobridge.nix b/nixos/modules/services/networking/jitsi-videobridge.nix
index eefaa70604cdd..09f2ddf92c5c7 100644
--- a/nixos/modules/services/networking/jitsi-videobridge.nix
+++ b/nixos/modules/services/networking/jitsi-videobridge.nix
@@ -150,7 +150,7 @@ in
         config = {
           hostName = mkDefault name;
           mucNickname = mkDefault (builtins.replaceStrings [ "." ] [ "-" ] (
-            config.networking.hostName + optionalString (config.networking.domain != null) ".${config.networking.domain}"
+            config.networking.fqdnOrHostName
           ));
         };
       }));
diff --git a/nixos/modules/services/networking/magic-wormhole-mailbox-server.nix b/nixos/modules/services/networking/magic-wormhole-mailbox-server.nix
index 326abe8cfce45..9dd1f62350aff 100644
--- a/nixos/modules/services/networking/magic-wormhole-mailbox-server.nix
+++ b/nixos/modules/services/networking/magic-wormhole-mailbox-server.nix
@@ -9,7 +9,7 @@ let
 in
 {
   options.services.magic-wormhole-mailbox-server = {
-    enable = mkEnableOption (lib.mdDoc "Enable Magic Wormhole Mailbox Server");
+    enable = mkEnableOption (lib.mdDoc "Magic Wormhole Mailbox Server");
   };
 
   config = mkIf cfg.enable {
diff --git a/nixos/modules/services/networking/mosquitto.nix b/nixos/modules/services/networking/mosquitto.nix
index 5ada92adc9b53..6543eb34b4b26 100644
--- a/nixos/modules/services/networking/mosquitto.nix
+++ b/nixos/modules/services/networking/mosquitto.nix
@@ -56,8 +56,10 @@ let
         default = null;
         description = mdDoc ''
           Specifies the hashed password for the MQTT User.
-          To generate hashed password install `mosquitto`
-          package and use `mosquitto_passwd`.
+          To generate hashed password install the `mosquitto`
+          package and use `mosquitto_passwd`, then extract
+          the second field (after the `:`) from the generated
+          file.
         '';
       };
 
@@ -68,8 +70,9 @@ let
         description = mdDoc ''
           Specifies the path to a file containing the
           hashed password for the MQTT user.
-          To generate hashed password install `mosquitto`
-          package and use `mosquitto_passwd`.
+          To generate hashed password install the `mosquitto`
+          package and use `mosquitto_passwd`, then remove the
+          `username:` prefix from the generated file.
         '';
       };
 
diff --git a/nixos/modules/services/networking/mozillavpn.nix b/nixos/modules/services/networking/mozillavpn.nix
index 71cbb0470412a..cf962879b4210 100644
--- a/nixos/modules/services/networking/mozillavpn.nix
+++ b/nixos/modules/services/networking/mozillavpn.nix
@@ -1,13 +1,8 @@
 { config, lib, pkgs, ... }:
 
 {
-  options.services.mozillavpn.enable = lib.mkOption {
-    type = lib.types.bool;
-    default = false;
-    description = lib.mdDoc ''
-      Enable the Mozilla VPN daemon.
-    '';
-  };
+  options.services.mozillavpn.enable =
+    lib.mkEnableOption (lib.mdDoc "Mozilla VPN daemon");
 
   config = lib.mkIf config.services.mozillavpn.enable {
     environment.systemPackages = [ pkgs.mozillavpn ];
diff --git a/nixos/modules/services/networking/mullvad-vpn.nix b/nixos/modules/services/networking/mullvad-vpn.nix
index 42d55056084db..82e68bf92af1f 100644
--- a/nixos/modules/services/networking/mullvad-vpn.nix
+++ b/nixos/modules/services/networking/mullvad-vpn.nix
@@ -4,24 +4,54 @@ let
 in
 with lib;
 {
-  options.services.mullvad-vpn.enable = mkOption {
-    type = types.bool;
-    default = false;
-    description = lib.mdDoc ''
-      This option enables Mullvad VPN daemon.
-      This sets {option}`networking.firewall.checkReversePath` to "loose", which might be undesirable for security.
-    '';
+  options.services.mullvad-vpn = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        This option enables Mullvad VPN daemon.
+        This sets {option}`networking.firewall.checkReversePath` to "loose", which might be undesirable for security.
+      '';
+    };
+
+    enableExcludeWrapper = mkOption {
+      type = types.bool;
+      default = true;
+      description = lib.mdDoc ''
+        This option activates the wrapper that allows the use of mullvad-exclude.
+        Might have minor security impact, so consider disabling if you do not use the feature.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.mullvad;
+      defaultText = literalExpression "pkgs.mullvad";
+      description = lib.mdDoc ''
+        The Mullvad package to use. `pkgs.mullvad` only provides the CLI tool, `pkgs.mullvad-vpn` provides both the CLI and the GUI.
+      '';
+    };
   };
 
   config = mkIf cfg.enable {
     boot.kernelModules = [ "tun" ];
 
+    environment.systemPackages = [ cfg.package ];
+
     # mullvad-daemon writes to /etc/iproute2/rt_tables
     networking.iproute2.enable = true;
 
     # See https://github.com/NixOS/nixpkgs/issues/113589
     networking.firewall.checkReversePath = "loose";
 
+    # See https://github.com/NixOS/nixpkgs/issues/176603
+    security.wrappers.mullvad-exclude = mkIf cfg.enableExcludeWrapper {
+      setuid = true;
+      owner = "root";
+      group = "root";
+      source = "${cfg.package}/bin/mullvad-exclude";
+    };
+
     systemd.services.mullvad-daemon = {
       description = "Mullvad VPN daemon";
       wantedBy = [ "multi-user.target" ];
@@ -39,12 +69,12 @@ with lib;
       startLimitBurst = 5;
       startLimitIntervalSec = 20;
       serviceConfig = {
-        ExecStart = "${pkgs.mullvad}/bin/mullvad-daemon -v --disable-stdout-timestamps";
+        ExecStart = "${cfg.package}/bin/mullvad-daemon -v --disable-stdout-timestamps";
         Restart = "always";
         RestartSec = 1;
       };
     };
   };
 
-  meta.maintainers = with maintainers; [ ymarkus ];
+  meta.maintainers = with maintainers; [ patricksjackson ymarkus ];
 }
diff --git a/nixos/modules/services/networking/nats.nix b/nixos/modules/services/networking/nats.nix
index dd732d2a9fca4..6c21e21b5cb88 100644
--- a/nixos/modules/services/networking/nats.nix
+++ b/nixos/modules/services/networking/nats.nix
@@ -137,7 +137,7 @@ in {
           RestrictNamespaces = true;
           RestrictRealtime = true;
           RestrictSUIDSGID = true;
-          SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+          SystemCallFilter = [ "@system-service" "~@privileged" ];
           UMask = "0077";
         }
       ];
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
index 0aa301251bb64..3b28cec83cb7a 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -365,7 +365,7 @@ in {
           If you enable this option the
           `networkmanager_strongswan` plugin will be added to
           the {option}`networking.networkmanager.plugins` option
-          so you don't need to to that yourself.
+          so you don't need to do that yourself.
         '';
       };
 
diff --git a/nixos/modules/services/networking/nntp-proxy.nix b/nixos/modules/services/networking/nntp-proxy.nix
index 06a8bb8b87d7f..b887c0e16ef43 100644
--- a/nixos/modules/services/networking/nntp-proxy.nix
+++ b/nixos/modules/services/networking/nntp-proxy.nix
@@ -71,7 +71,7 @@ in
       };
 
       upstreamPort = mkOption {
-        type = types.int;
+        type = types.port;
         default = 563;
         description = lib.mdDoc ''
           Upstream server port
@@ -112,7 +112,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 5555;
         description = lib.mdDoc ''
           Proxy listen port
diff --git a/nixos/modules/services/networking/ntp/chrony.nix b/nixos/modules/services/networking/ntp/chrony.nix
index a89c7769152e6..7e3bb565d10bf 100644
--- a/nixos/modules/services/networking/ntp/chrony.nix
+++ b/nixos/modules/services/networking/ntp/chrony.nix
@@ -27,7 +27,7 @@ let
     ${cfg.extraConfig}
   '';
 
-  chronyFlags = "-n -m -u chrony -f ${configFile} ${toString cfg.extraFlags}";
+  chronyFlags = [ "-n" "-m" "-u" "chrony" "-f" "${configFile}" ] ++ cfg.extraFlags;
 in
 {
   options = {
@@ -166,7 +166,7 @@ in
         unitConfig.ConditionCapability = "CAP_SYS_TIME";
         serviceConfig =
           { Type = "simple";
-            ExecStart = "${chronyPkg}/bin/chronyd ${chronyFlags}";
+            ExecStart = "${chronyPkg}/bin/chronyd ${builtins.toString chronyFlags}";
 
             ProtectHome = "yes";
             ProtectSystem = "full";
diff --git a/nixos/modules/services/networking/ntp/ntpd.nix b/nixos/modules/services/networking/ntp/ntpd.nix
index a9dae2c8667aa..036a8df635db0 100644
--- a/nixos/modules/services/networking/ntp/ntpd.nix
+++ b/nixos/modules/services/networking/ntp/ntpd.nix
@@ -25,7 +25,7 @@ let
     ${cfg.extraConfig}
   '';
 
-  ntpFlags = "-c ${configFile} -u ntp:ntp ${toString cfg.extraFlags}";
+  ntpFlags = [ "-c" "${configFile}" "-u" "ntp:ntp" ] ++ cfg.extraFlags;
 
 in
 
@@ -137,7 +137,7 @@ in
           '';
 
         serviceConfig = {
-          ExecStart = "@${ntp}/bin/ntpd ntpd -g ${ntpFlags}";
+          ExecStart = "@${ntp}/bin/ntpd ntpd -g ${builtins.toString ntpFlags}";
           Type = "forking";
         };
       };
diff --git a/nixos/modules/services/networking/onedrive.nix b/nixos/modules/services/networking/onedrive.nix
index 5a531d7a47fdc..d782ec05352b7 100644
--- a/nixos/modules/services/networking/onedrive.nix
+++ b/nixos/modules/services/networking/onedrive.nix
@@ -26,11 +26,7 @@ in {
   ### Interface
 
   options.services.onedrive = {
-    enable = lib.mkOption {
-      type = lib.types.bool;
-      default = false;
-      description = lib.mdDoc "Enable OneDrive service";
-    };
+     enable = lib.mkEnableOption (lib.mdDoc "OneDrive service");
 
      package = lib.mkOption {
        type = lib.types.package;
diff --git a/nixos/modules/services/networking/ostinato.nix b/nixos/modules/services/networking/ostinato.nix
index 1e4dcf37f64f4..40c227ea0c684 100644
--- a/nixos/modules/services/networking/ostinato.nix
+++ b/nixos/modules/services/networking/ostinato.nix
@@ -29,7 +29,7 @@ in
       enable = mkEnableOption (lib.mdDoc "Ostinato agent-controller (Drone)");
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 7878;
         description = lib.mdDoc ''
           Port to listen on.
diff --git a/nixos/modules/services/networking/owamp.nix b/nixos/modules/services/networking/owamp.nix
index e7a5bfea5258e..32b2dab9e3c74 100644
--- a/nixos/modules/services/networking/owamp.nix
+++ b/nixos/modules/services/networking/owamp.nix
@@ -10,7 +10,7 @@ in
   ###### interface
 
   options = {
-    services.owamp.enable = mkEnableOption (lib.mdDoc "Enable OWAMP server");
+    services.owamp.enable = mkEnableOption (lib.mdDoc "OWAMP server");
   };
 
 
diff --git a/nixos/modules/services/networking/polipo.nix b/nixos/modules/services/networking/polipo.nix
index d820e1b397b81..8581553829bfa 100644
--- a/nixos/modules/services/networking/polipo.nix
+++ b/nixos/modules/services/networking/polipo.nix
@@ -23,11 +23,7 @@ in
 
     services.polipo = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Whether to run the polipo caching web proxy.";
-      };
+      enable = mkEnableOption (lib.mdDoc "polipo caching web proxy");
 
       proxyAddress = mkOption {
         type = types.str;
@@ -36,7 +32,7 @@ in
       };
 
       proxyPort = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8123;
         description = lib.mdDoc "TCP port on which Polipo will listen.";
       };
diff --git a/nixos/modules/services/networking/shadowsocks.nix b/nixos/modules/services/networking/shadowsocks.nix
index 8eee40711a6d2..2034dca6f26b2 100644
--- a/nixos/modules/services/networking/shadowsocks.nix
+++ b/nixos/modules/services/networking/shadowsocks.nix
@@ -48,7 +48,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8388;
         description = lib.mdDoc ''
           Port which the server uses.
diff --git a/nixos/modules/services/networking/smokeping.nix b/nixos/modules/services/networking/smokeping.nix
index df4f8905ec6f8..2e67f8b77c088 100644
--- a/nixos/modules/services/networking/smokeping.nix
+++ b/nixos/modules/services/networking/smokeping.nix
@@ -49,11 +49,8 @@ in
 {
   options = {
     services.smokeping = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Enable the smokeping service";
-      };
+      enable = mkEnableOption (lib.mdDoc "smokeping service");
+
       alertConfig = mkOption {
         type = types.lines;
         default = ''
@@ -186,7 +183,7 @@ in
         '';
       };
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8081;
         description = lib.mdDoc "TCP port to use for the web server.";
       };
@@ -318,6 +315,17 @@ in
       description = "smokeping daemon user";
       home = smokepingHome;
       createHome = true;
+      # When `cfg.webService` is enabled, `thttpd` makes SmokePing available
+      # under `${cfg.host}:${cfg.port}/smokeping.fcgi` as per the `ln -s` below.
+      # We also want that going to `${cfg.host}:${cfg.port}` without `smokeping.fcgi`
+      # makes it easy for the user to find SmokePing.
+      # However `thttpd` does not seem to support easy redirections from `/` to `smokeping.fcgi`
+      # and only allows directory listings or `/` -> `index.html` resolution if the directory
+      # has `chmod 755` (see https://acme.com/software/thttpd/thttpd_man.html#PERMISSIONS,
+      # " directories should be 755 if you want to allow indexing").
+      # Otherwise it shows `403 Forbidden` on `/`.
+      # Thus, we need to make `smokepingHome` (which is given to `thttpd -d` below) `755`.
+      homeMode = "755";
     };
     users.groups.${cfg.user} = {};
     systemd.services.smokeping = {
diff --git a/nixos/modules/services/networking/snowflake-proxy.nix b/nixos/modules/services/networking/snowflake-proxy.nix
index 7299db7a53e82..ca015ed9d44bc 100644
--- a/nixos/modules/services/networking/snowflake-proxy.nix
+++ b/nixos/modules/services/networking/snowflake-proxy.nix
@@ -71,7 +71,7 @@ in
         RestrictNamespaces = true;
         RestrictRealtime = true;
         SystemCallArchitectures = "native";
-        SystemCallFilter = "~@clock @cpu-emulation @debug @mount @obsolete @reboot @swap @privileged @resources";
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
         UMask = "0077";
       };
     };
diff --git a/nixos/modules/services/networking/sslh.nix b/nixos/modules/services/networking/sslh.nix
index 9d76d69152fa6..daf2f2f3668ee 100644
--- a/nixos/modules/services/networking/sslh.nix
+++ b/nixos/modules/services/networking/sslh.nix
@@ -70,7 +70,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 443;
         description = lib.mdDoc "Listening port.";
       };
diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
index 0876007a6e732..adbb25ccb9b6d 100644
--- a/nixos/modules/services/networking/syncthing.nix
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -55,8 +55,8 @@ let
 
     # generate the new config by merging with the NixOS config options
     new_cfg=$(printf '%s\n' "$old_cfg" | ${pkgs.jq}/bin/jq -c '. * {
-        "devices": (${builtins.toJSON devices}${optionalString (! cfg.overrideDevices) " + .devices"}),
-        "folders": (${builtins.toJSON folders}${optionalString (! cfg.overrideFolders) " + .folders"})
+        "devices": (${builtins.toJSON devices}${optionalString (cfg.devices == {} || ! cfg.overrideDevices) " + .devices"}),
+        "folders": (${builtins.toJSON folders}${optionalString (cfg.folders == {} || ! cfg.overrideFolders) " + .folders"})
     } * ${builtins.toJSON cfg.extraOptions}')
 
     # send the new config
@@ -212,10 +212,18 @@ in {
             };
 
             path = mkOption {
-              type = types.str;
+              # TODO for release 23.05: allow relative paths again and set
+              # working directory to cfg.dataDir
+              type = types.str // {
+                check = x: types.str.check x && (substring 0 1 x == "/" || substring 0 2 x == "~/");
+                description = types.str.description + " starting with / or ~/";
+              };
               default = name;
               description = lib.mdDoc ''
                 The path to the folder which should be shared.
+                Only absolute paths (starting with `/`) and paths relative to
+                the [user](#opt-services.syncthing.user)'s home directory
+                (starting with `~/`) are allowed.
               '';
             };
 
@@ -405,7 +413,8 @@ in {
         example = "yourUser";
         description = mdDoc ''
           The user to run Syncthing as.
-          By default, a user named `${defaultUser}` will be created.
+          By default, a user named `${defaultUser}` will be created whose home
+          directory is [dataDir](#opt-services.syncthing.dataDir).
         '';
       };
 
diff --git a/nixos/modules/services/networking/teleport.nix b/nixos/modules/services/networking/teleport.nix
index 802907a00dc50..6433554f87dab 100644
--- a/nixos/modules/services/networking/teleport.nix
+++ b/nixos/modules/services/networking/teleport.nix
@@ -65,7 +65,7 @@ in
         };
 
         port = mkOption {
-          type = int;
+          type = port;
           default = 3000;
           description = lib.mdDoc "Metrics and diagnostics port.";
         };
diff --git a/nixos/modules/services/networking/tox-bootstrapd.nix b/nixos/modules/services/networking/tox-bootstrapd.nix
index e6dc36bf9ecfc..5c7e7a4c22082 100644
--- a/nixos/modules/services/networking/tox-bootstrapd.nix
+++ b/nixos/modules/services/networking/tox-bootstrapd.nix
@@ -29,7 +29,7 @@ in
           };
 
           port = mkOption {
-            type = types.int;
+            type = types.port;
             default = 33445;
             description = lib.mdDoc "Listening port (UDP).";
           };
diff --git a/nixos/modules/services/networking/toxvpn.nix b/nixos/modules/services/networking/toxvpn.nix
index d0ff5bc4e8138..3a14b5f73091c 100644
--- a/nixos/modules/services/networking/toxvpn.nix
+++ b/nixos/modules/services/networking/toxvpn.nix
@@ -14,7 +14,7 @@ with lib;
       };
 
       port = mkOption {
-        type        = types.int;
+        type        = types.port;
         default     = 33445;
         description = lib.mdDoc "udp port for toxcore, port-forward to help with connectivity if you run many nodes behind one NAT";
       };
diff --git a/nixos/modules/services/networking/vdirsyncer.nix b/nixos/modules/services/networking/vdirsyncer.nix
new file mode 100644
index 0000000000000..6a069943434da
--- /dev/null
+++ b/nixos/modules/services/networking/vdirsyncer.nix
@@ -0,0 +1,214 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.vdirsyncer;
+
+  toIniJson = with generators; toINI {
+    mkKeyValue = mkKeyValueDefault {
+      mkValueString = builtins.toJSON;
+    } "=";
+  };
+
+  toConfigFile = name: cfg':
+    if
+      cfg'.configFile != null
+    then
+      cfg'.configFile
+    else
+      pkgs.writeText "vdirsyncer-${name}.conf" (toIniJson (
+        {
+          general = cfg'.config.general // (lib.optionalAttrs (cfg'.config.statusPath == null) {
+            status_path = "/var/lib/vdirsyncer/${name}";
+          });
+        } // (
+          mapAttrs' (name: nameValuePair "pair ${name}") cfg'.config.pairs
+        ) // (
+          mapAttrs' (name: nameValuePair "storage ${name}") cfg'.config.storages
+        )
+      ));
+
+  userUnitConfig = name: cfg': {
+    serviceConfig = {
+      User = if cfg'.user == null then "vdirsyncer" else cfg'.user;
+      Group = if cfg'.group == null then "vdirsyncer" else cfg'.group;
+    }  // (optionalAttrs (cfg'.user == null) {
+      DynamicUser = true;
+    }) // (optionalAttrs (cfg'.additionalGroups != []) {
+      SupplementaryGroups = cfg'.additionalGroups;
+    }) // (optionalAttrs (cfg'.config.statusPath == null) {
+      StateDirectory = "vdirsyncer/${name}";
+      StateDirectoryMode = "0700";
+    });
+  };
+
+  commonUnitConfig = {
+    after = [ "network.target" ];
+    serviceConfig = {
+      Type = "oneshot";
+      # Sandboxing
+      PrivateTmp = true;
+      NoNewPrivileges = true;
+      ProtectSystem = "strict";
+      ProtectHome = true;
+      ProtectKernelTunables = true;
+      ProtectKernelModules = true;
+      ProtectControlGroups = true;
+      RestrictNamespaces = true;
+      MemoryDenyWriteExecute = true;
+      RestrictRealtime = true;
+      RestrictSUIDSGID = true;
+      RestrictAddressFamilies = "AF_INET AF_INET6";
+      LockPersonality = true;
+    };
+  };
+
+in
+{
+  options = {
+    services.vdirsyncer = {
+      enable = mkEnableOption (mdDoc "vdirsyncer");
+
+      package = mkPackageOption pkgs "vdirsyncer" {};
+
+      jobs = mkOption {
+        description = mdDoc "vdirsyncer job configurations";
+        type = types.attrsOf (types.submodule {
+          options = {
+            enable = (mkEnableOption (mdDoc "this vdirsyncer job")) // {
+              default = true;
+              example = false;
+            };
+
+            user = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = mdDoc ''
+                User account to run vdirsyncer as, otherwise as a systemd
+                dynamic user
+              '';
+            };
+
+            group = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = mdDoc "group to run vdirsyncer as";
+            };
+
+            additionalGroups = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = mdDoc "additional groups to add the dynamic user to";
+            };
+
+            forceDiscover = mkOption {
+              type = types.bool;
+              default = false;
+              description = mdDoc ''
+                Run `yes | vdirsyncer discover` prior to `vdirsyncer sync`
+              '';
+            };
+
+            timerConfig = mkOption {
+              type = types.attrs;
+              default = {
+                OnBootSec = "1h";
+                OnUnitActiveSec = "6h";
+              };
+              description = mdDoc "systemd timer configuration";
+            };
+
+            configFile = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = mdDoc "existing configuration file";
+            };
+
+            config = {
+              statusPath = mkOption {
+                type = types.nullOr types.str;
+                default = null;
+                defaultText = literalExpression "/var/lib/vdirsyncer/\${attrName}";
+                description = mdDoc "vdirsyncer's status path";
+              };
+
+              general = mkOption {
+                type = types.attrs;
+                default = {};
+                description = mdDoc "general configuration";
+              };
+
+              pairs = mkOption {
+                type = types.attrsOf types.attrs;
+                default = {};
+                description = mdDoc "vdirsyncer pair configurations";
+                example = literalExpression ''
+                  {
+                    my_contacts = {
+                      a = "my_cloud_contacts";
+                      b = "my_local_contacts";
+                      collections = [ "from a" ];
+                      conflict_resolution = "a wins";
+                      metadata = [ "color" "displayname" ];
+                    };
+                  };
+                '';
+              };
+
+              storages = mkOption {
+                type = types.attrsOf types.attrs;
+                default = {};
+                description = mdDoc "vdirsyncer storage configurations";
+                example = literalExpression ''
+                  {
+                    my_cloud_contacts = {
+                      type = "carddav";
+                      url = "https://dav.example.com/";
+                      read_only = true;
+                      username = "user";
+                      "password.fetch" = [ "command" "cat" "/etc/vdirsyncer/cloud.passwd" ];
+                    };
+                    my_local_contacts = {
+                      type = "carddav";
+                      url = "https://localhost/";
+                      username = "user";
+                      "password.fetch" = [ "command" "cat" "/etc/vdirsyncer/local.passwd" ];
+                    };
+                  }
+                '';
+              };
+            };
+          };
+        });
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services = mapAttrs' (name: cfg': nameValuePair "vdirsyncer@${name}" (
+      foldr recursiveUpdate {} [
+        commonUnitConfig
+        (userUnitConfig name cfg')
+        {
+          description = "synchronize calendars and contacts (${name})";
+          environment.VDIRSYNCER_CONFIG = toConfigFile name cfg';
+          serviceConfig.ExecStart =
+            (optional cfg'.forceDiscover (
+              pkgs.writeShellScript "vdirsyncer-discover-yes" ''
+                set -e
+                yes | ${cfg.package}/bin/vdirsyncer discover
+              ''
+            )) ++ [ "${cfg.package}/bin/vdirsyncer sync" ];
+        }
+      ]
+    )) (filterAttrs (name: cfg': cfg'.enable) cfg.jobs);
+
+    systemd.timers = mapAttrs' (name: cfg': nameValuePair "vdirsyncer@${name}" {
+      wantedBy = [ "timers.target" ];
+      description = "synchronize calendars and contacts (${name})";
+      inherit (cfg') timerConfig;
+    }) cfg.jobs;
+  };
+}
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index 3f6fa3c864026..e3c3d3ba3c962 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -391,6 +391,19 @@ let
         '';
       };
 
+  # the target is required to start new peer units when they are added
+  generateInterfaceTarget = name: values:
+    let
+      mkPeerUnit = peer: (peerUnitServiceName name peer.publicKey (peer.dynamicEndpointRefreshSeconds != 0)) + ".service";
+    in
+    nameValuePair "wireguard-${name}"
+      rec {
+        description = "WireGuard Tunnel - ${name}";
+        wantedBy = [ "multi-user.target" ];
+        wants = [ "wireguard-${name}.service" ] ++ map mkPeerUnit values.peers;
+        after = wants;
+      };
+
   generateInterfaceUnit = name: values:
     # exactly one way to specify the private key must be set
     #assert (values.privateKey != null) != (values.privateKeyFile != null);
@@ -409,7 +422,6 @@ let
         after = [ "network-pre.target" ];
         wants = [ "network.target" ];
         before = [ "network.target" ];
-        wantedBy = [ "multi-user.target" ];
         environment.DEVICE = name;
         path = with pkgs; [ kmod iproute2 wireguard-tools ];
 
@@ -540,6 +552,8 @@ in
       // (mapAttrs' generateKeyServiceUnit
       (filterAttrs (name: value: value.generatePrivateKeyFile) cfg.interfaces));
 
-  });
+      systemd.targets = mapAttrs' generateInterfaceTarget cfg.interfaces;
+    }
+  );
 
 }
diff --git a/nixos/modules/services/networking/xray.nix b/nixos/modules/services/networking/xray.nix
new file mode 100644
index 0000000000000..e2fd83c4dfd9b
--- /dev/null
+++ b/nixos/modules/services/networking/xray.nix
@@ -0,0 +1,96 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  options = {
+
+    services.xray = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Whether to run xray server.
+
+          Either `settingsFile` or `settings` must be specified.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.xray;
+        defaultText = literalExpression "pkgs.xray";
+        description = lib.mdDoc ''
+          Which xray package to use.
+        '';
+      };
+
+      settingsFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/etc/xray/config.json";
+        description = lib.mdDoc ''
+          The absolute path to the configuration file.
+
+          Either `settingsFile` or `settings` must be specified.
+
+          See <https://www.v2fly.org/en_US/config/overview.html>.
+        '';
+      };
+
+      settings = mkOption {
+        type = types.nullOr (types.attrsOf types.unspecified);
+        default = null;
+        example = {
+          inbounds = [{
+            port = 1080;
+            listen = "127.0.0.1";
+            protocol = "http";
+          }];
+          outbounds = [{
+            protocol = "freedom";
+          }];
+        };
+        description = lib.mdDoc ''
+          The configuration object.
+
+          Either `settingsFile` or `settings` must be specified.
+
+          See <https://www.v2fly.org/en_US/config/overview.html>.
+        '';
+      };
+    };
+
+  };
+
+  config = let
+    cfg = config.services.xray;
+    settingsFile = if cfg.settingsFile != null
+      then cfg.settingsFile
+      else pkgs.writeTextFile {
+        name = "xray.json";
+        text = builtins.toJSON cfg.settings;
+        checkPhase = ''
+          ${cfg.package}/bin/xray -test -config $out
+        '';
+      };
+
+  in mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = (cfg.settingsFile == null) != (cfg.settings == null);
+        message = "Either but not both `settingsFile` and `settings` should be specified for xray.";
+      }
+    ];
+
+    systemd.services.xray = {
+      description = "xray Daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${cfg.package}/bin/xray -config ${settingsFile}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/xrdp.nix b/nixos/modules/services/networking/xrdp.nix
index 554fb66f36eb0..ed7f1dadd3707 100644
--- a/nixos/modules/services/networking/xrdp.nix
+++ b/nixos/modules/services/networking/xrdp.nix
@@ -54,7 +54,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 3389;
         description = lib.mdDoc ''
           Specifies on which port the xrdp daemon listens.
diff --git a/nixos/modules/services/networking/yggdrasil.nix b/nixos/modules/services/networking/yggdrasil.nix
index e56f169d05ebf..3d5cbdd2dc3ed 100644
--- a/nixos/modules/services/networking/yggdrasil.nix
+++ b/nixos/modules/services/networking/yggdrasil.nix
@@ -180,7 +180,7 @@ in {
         RestrictNamespaces = true;
         RestrictRealtime = true;
         SystemCallArchitectures = "native";
-        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @resources";
+        SystemCallFilter = [ "@system-service" "~@privileged @keyring" ];
       } // (if (cfg.group != null) then {
         Group = cfg.group;
       } else {});
diff --git a/nixos/modules/services/networking/zerotierone.nix b/nixos/modules/services/networking/zerotierone.nix
index 30f75cd85d436..0d9e25cfc52cf 100644
--- a/nixos/modules/services/networking/zerotierone.nix
+++ b/nixos/modules/services/networking/zerotierone.nix
@@ -19,7 +19,7 @@ in
 
   options.services.zerotierone.port = mkOption {
     default = 9993;
-    type = types.int;
+    type = types.port;
     description = lib.mdDoc ''
       Network port used by ZeroTier.
     '';
diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix
index fea7ffb673ca0..ae59dcc226de8 100644
--- a/nixos/modules/services/printing/cupsd.nix
+++ b/nixos/modules/services/printing/cupsd.nix
@@ -134,6 +134,15 @@ in
         '';
       };
 
+      stateless = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          If set, all state directories relating to CUPS will be removed on
+          startup of the service.
+        '';
+      };
+
       startWhenNeeded = mkOption {
         type = types.bool;
         default = true;
@@ -343,8 +352,9 @@ in
 
         path = [ cups.out ];
 
-        preStart =
-          ''
+        preStart = lib.optionalString cfg.stateless ''
+          rm -rf /var/cache/cups /var/lib/cups /var/spool/cups
+        '' + ''
             mkdir -m 0700 -p /var/cache/cups
             mkdir -m 0700 -p /var/spool/cups
             mkdir -m 0755 -p ${cfg.tempDir}
diff --git a/nixos/modules/services/search/hound.nix b/nixos/modules/services/search/hound.nix
index c81ceee546965..b41a2e2bae1fa 100644
--- a/nixos/modules/services/search/hound.nix
+++ b/nixos/modules/services/search/hound.nix
@@ -120,7 +120,6 @@ in {
                     " -conf ${pkgs.writeText "hound.json" cfg.config}";
 
       };
-      path = [ pkgs.git pkgs.mercurial pkgs.openssh ];
     };
   };
 
diff --git a/nixos/modules/services/search/solr.nix b/nixos/modules/services/search/solr.nix
index 48570412b0c20..05592e9fa247d 100644
--- a/nixos/modules/services/search/solr.nix
+++ b/nixos/modules/services/search/solr.nix
@@ -21,7 +21,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8983;
         description = lib.mdDoc "Port on which Solr is ran.";
       };
diff --git a/nixos/modules/services/security/endlessh-go.nix b/nixos/modules/services/security/endlessh-go.nix
index 61cca55317394..6557ec953cd86 100644
--- a/nixos/modules/services/security/endlessh-go.nix
+++ b/nixos/modules/services/security/endlessh-go.nix
@@ -126,7 +126,7 @@ in
           RestrictRealtime = true;
           RestrictSUIDSGID = true;
           SystemCallArchitectures = "native";
-          SystemCallFilter = [ "@system-service" "~@resources" "~@privileged" ];
+          SystemCallFilter = [ "@system-service" "~@privileged" ];
         };
     };
 
diff --git a/nixos/modules/services/security/endlessh.nix b/nixos/modules/services/security/endlessh.nix
new file mode 100644
index 0000000000000..e99b4dadcd581
--- /dev/null
+++ b/nixos/modules/services/security/endlessh.nix
@@ -0,0 +1,99 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.endlessh;
+in
+{
+  options.services.endlessh = {
+    enable = mkEnableOption (mdDoc "endlessh service");
+
+    port = mkOption {
+      type = types.port;
+      default = 2222;
+      example = 22;
+      description = mdDoc ''
+        Specifies on which port the endlessh daemon listens for SSH
+        connections.
+
+        Setting this to `22` may conflict with {option}`services.openssh`.
+      '';
+    };
+
+    extraOptions = mkOption {
+      type = with types; listOf str;
+      default = [ ];
+      example = [ "-6" "-d 9000" "-v" ];
+      description = mdDoc ''
+        Additional command line options to pass to the endlessh daemon.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Whether to open a firewall port for the SSH listener.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.endlessh = {
+      description = "SSH tarpit";
+      requires = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig =
+        let
+          needsPrivileges = cfg.port < 1024;
+          capabilities = [ "" ] ++ optionals needsPrivileges [ "CAP_NET_BIND_SERVICE" ];
+          rootDirectory = "/run/endlessh";
+        in
+        {
+          Restart = "always";
+          ExecStart = with cfg; concatStringsSep " " ([
+            "${pkgs.endlessh}/bin/endlessh"
+            "-p ${toString port}"
+          ] ++ extraOptions);
+          DynamicUser = true;
+          RootDirectory = rootDirectory;
+          BindReadOnlyPaths = [ builtins.storeDir ];
+          InaccessiblePaths = [ "-+${rootDirectory}" ];
+          RuntimeDirectory = baseNameOf rootDirectory;
+          RuntimeDirectoryMode = "700";
+          AmbientCapabilities = capabilities;
+          CapabilityBoundingSet = capabilities;
+          UMask = "0077";
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
+          PrivateDevices = true;
+          PrivateTmp = true;
+          PrivateUsers = !needsPrivileges;
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectSystem = "strict";
+          ProtectProc = "noaccess";
+          ProcSubset = "pid";
+          RemoveIPC = true;
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [ "@system-service" "~@resources" "~@privileged" ];
+        };
+    };
+
+    networking.firewall.allowedTCPPorts = with cfg;
+      optionals openFirewall [ port ];
+  };
+
+  meta.maintainers = with maintainers; [ azahi ];
+}
diff --git a/nixos/modules/services/security/tor.nix b/nixos/modules/services/security/tor.nix
index 730802d92cfa8..b85b78f269a1d 100644
--- a/nixos/modules/services/security/tor.nix
+++ b/nixos/modules/services/security/tor.nix
@@ -816,13 +816,13 @@ in
         always create a container/VM with a separate Tor daemon instance.
       '' ++
       flatten (mapAttrsToList (n: o:
-        optional (o.settings.HiddenServiceVersion == 2) [
+        optionals (o.settings.HiddenServiceVersion == 2) [
           (optional (o.settings.HiddenServiceExportCircuitID != null) ''
             HiddenServiceExportCircuitID is used in the HiddenService: ${n}
             but this option is only for v3 hidden services.
           '')
         ] ++
-        optional (o.settings.HiddenServiceVersion != 2) [
+        optionals (o.settings.HiddenServiceVersion != 2) [
           (optional (o.settings.HiddenServiceAuthorizeClient != null) ''
             HiddenServiceAuthorizeClient is used in the HiddenService: ${n}
             but this option is only for v2 hidden services.
diff --git a/nixos/modules/services/system/automatic-timezoned.nix b/nixos/modules/services/system/automatic-timezoned.nix
new file mode 100644
index 0000000000000..9bdd64dd33a3c
--- /dev/null
+++ b/nixos/modules/services/system/automatic-timezoned.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.automatic-timezoned;
+in
+{
+  options = {
+    services.automatic-timezoned = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = mdDoc ''
+          Enable `automatic-timezoned`, simple daemon for keeping the system
+          timezone up-to-date based on the current location. It uses geoclue2 to
+          determine the current location and systemd-timedated to actually set
+          the timezone.
+        '';
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.automatic-timezoned;
+        defaultText = literalExpression "pkgs.automatic-timezoned";
+        description = mdDoc ''
+          Which `automatic-timezoned` package to use.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    security.polkit.extraConfig = ''
+      polkit.addRule(function(action, subject) {
+        if (action.id == "org.freedesktop.timedate1.set-timezone"
+            && subject.user == "automatic-timezoned") {
+          return polkit.Result.YES;
+        }
+      });
+    '';
+
+    services.geoclue2 = {
+      enable = true;
+      appConfig.automatic-timezoned = {
+        isAllowed = true;
+        isSystem = true;
+        users = [ (toString config.ids.uids.automatic-timezoned) ];
+      };
+    };
+
+    systemd.services = {
+
+      automatic-timezoned = {
+        description = "Automatically update system timezone based on location";
+        requires = [ "automatic-timezoned-geoclue-agent.service" ];
+        after = [ "automatic-timezoned-geoclue-agent.service" ];
+        serviceConfig = {
+          Type = "exec";
+          User = "automatic-timezoned";
+          ExecStart = "${cfg.package}/bin/automatic-timezoned --zoneinfo-path=${pkgs.tzdata}/share/zoneinfo/zone1970.tab";
+        };
+        wantedBy = [ "default.target" ];
+      };
+
+      automatic-timezoned-geoclue-agent = {
+        description = "Geoclue agent for automatic-timezoned";
+        requires = [ "geoclue.service" ];
+        after = [ "geoclue.service" ];
+        serviceConfig = {
+          Type = "exec";
+          User = "automatic-timezoned";
+          ExecStart = "${pkgs.geoclue2-with-demo-agent}/libexec/geoclue-2.0/demos/agent";
+          Restart = "on-failure";
+          PrivateTmp = true;
+        };
+        wantedBy = [ "default.target" ];
+      };
+
+    };
+
+    users = {
+      users.automatic-timezoned = {
+        description = "automatic-timezoned";
+        uid = config.ids.uids.automatic-timezoned;
+        group = "automatic-timezoned";
+      };
+      groups.automatic-timezoned = {
+        gid = config.ids.gids.automatic-timezoned;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/system/cloud-init.nix b/nixos/modules/services/system/cloud-init.nix
index 95ca5fb48dbae..30b2ca568e12f 100644
--- a/nixos/modules/services/system/cloud-init.nix
+++ b/nixos/modules/services/system/cloud-init.nix
@@ -81,7 +81,8 @@ in
            - write-files
            - growpart
            - resizefs
-           - update_etc_hosts
+           - update_hostname
+           - resolv_conf
            - ca-certs
            - rsyslog
            - users-groups
diff --git a/nixos/modules/services/system/nscd.nix b/nixos/modules/services/system/nscd.nix
index 0a59feb706642..fdc5190d084b3 100644
--- a/nixos/modules/services/system/nscd.nix
+++ b/nixos/modules/services/system/nscd.nix
@@ -27,6 +27,15 @@ in
         '';
       };
 
+      enableNsncd = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Whether to use nsncd instead of nscd.
+          This is a nscd-compatible daemon, that proxies lookups, without any caching.
+        '';
+      };
+
       user = mkOption {
         type = types.str;
         default = "nscd";
@@ -51,7 +60,8 @@ in
 
       package = mkOption {
         type = types.package;
-        default = if pkgs.stdenv.hostPlatform.libc == "glibc"
+        default =
+          if pkgs.stdenv.hostPlatform.libc == "glibc"
           then pkgs.stdenv.cc.libc.bin
           else pkgs.glibc.bin;
         defaultText = lib.literalExpression ''
@@ -59,7 +69,10 @@ in
             then pkgs.stdenv.cc.libc.bin
             else pkgs.glibc.bin;
         '';
-        description = lib.mdDoc "package containing the nscd binary to be used by the service";
+        description = lib.mdDoc ''
+          package containing the nscd binary to be used by the service.
+          Ignored when enableNsncd is set to true.
+        '';
       };
 
     };
@@ -77,10 +90,12 @@ in
       group = cfg.group;
     };
 
-    users.groups.${cfg.group} = {};
+    users.groups.${cfg.group} = { };
 
     systemd.services.nscd =
-      { description = "Name Service Cache Daemon";
+      {
+        description = "Name Service Cache Daemon"
+          + lib.optionalString cfg.enableNsncd " (nsncd)";
 
         before = [ "nss-lookup.target" "nss-user-lookup.target" ];
         wants = [ "nss-lookup.target" "nss-user-lookup.target" ];
@@ -89,14 +104,14 @@ in
 
         environment = { LD_LIBRARY_PATH = nssModulesPath; };
 
-        restartTriggers = [
+        restartTriggers = lib.optionals (!cfg.enableNsncd) ([
           config.environment.etc.hosts.source
           config.environment.etc."nsswitch.conf".source
           config.environment.etc."nscd.conf".source
         ] ++ optionals config.users.mysql.enable [
           config.environment.etc."libnss-mysql.cfg".source
           config.environment.etc."libnss-mysql-root.cfg".source
-        ];
+        ]);
 
         # In some configurations, nscd needs to be started as root; it will
         # drop privileges after all the NSS modules have read their
@@ -106,8 +121,11 @@ in
         # sill want to read their configuration files after the privilege drop
         # and so users can set the owner of those files to the nscd user.
         serviceConfig =
-          { ExecStart = "!@${cfg.package}/bin/nscd nscd";
-            Type = "forking";
+          {
+            ExecStart =
+              if cfg.enableNsncd then "${pkgs.nsncd}/bin/nsncd"
+              else "!@${cfg.package}/bin/nscd nscd";
+            Type = if cfg.enableNsncd then "notify" else "forking";
             User = cfg.user;
             Group = cfg.group;
             RemoveIPC = true;
@@ -120,12 +138,12 @@ in
             PIDFile = "/run/nscd/nscd.pid";
             Restart = "always";
             ExecReload =
-              [ "${cfg.package}/bin/nscd --invalidate passwd"
+              lib.optionals (!cfg.enableNsncd) [
+                "${cfg.package}/bin/nscd --invalidate passwd"
                 "${cfg.package}/bin/nscd --invalidate group"
                 "${cfg.package}/bin/nscd --invalidate hosts"
               ];
           };
       };
-
   };
 }
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index 5e0d13211bcf3..9b53f5de143d4 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -431,7 +431,7 @@ in
       # https://trac.transmissionbt.com/browser/trunk/libtransmission/tr-udp.c?rev=11956.
       # at least up to the values hardcoded here:
       (mkIf cfg.settings.utp-enabled {
-        "net.core.rmem_max" = mkDefault "4194304"; # 4MB
+        "net.core.rmem_max" = mkDefault 4194304; # 4MB
         "net.core.wmem_max" = mkDefault "1048576"; # 1MB
       })
       (mkIf cfg.performanceNetParameters {
diff --git a/nixos/modules/services/ttys/getty.nix b/nixos/modules/services/ttys/getty.nix
index aec65903cecb9..22ae9c27e5bc6 100644
--- a/nixos/modules/services/ttys/getty.nix
+++ b/nixos/modules/services/ttys/getty.nix
@@ -146,7 +146,7 @@ in
         enable = mkDefault config.boot.isContainer;
       };
 
-    environment.etc.issue =
+    environment.etc.issue = mkDefault
       { # Friendly greeting on the virtual consoles.
         source = pkgs.writeText "issue" ''
 
diff --git a/nixos/modules/services/web-apps/alps.nix b/nixos/modules/services/web-apps/alps.nix
index b171729fd0a35..b73cb82925d9b 100644
--- a/nixos/modules/services/web-apps/alps.nix
+++ b/nixos/modules/services/web-apps/alps.nix
@@ -54,7 +54,7 @@ in {
     smtps = {
       port = mkOption {
         type = types.port;
-        default = 445;
+        default = 465;
         description = lib.mdDoc ''
           The SMTPS server port.
         '';
diff --git a/nixos/modules/services/web-apps/atlassian/crowd.nix b/nixos/modules/services/web-apps/atlassian/crowd.nix
index ac571e1888068..c8d1eaef31d88 100644
--- a/nixos/modules/services/web-apps/atlassian/crowd.nix
+++ b/nixos/modules/services/web-apps/atlassian/crowd.nix
@@ -61,7 +61,7 @@ in
       };
 
       listenPort = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8092;
         description = lib.mdDoc "Port to listen on.";
       };
@@ -95,7 +95,7 @@ in
         };
 
         port = mkOption {
-          type = types.int;
+          type = types.port;
           default = 443;
           example = 80;
           description = lib.mdDoc "Port used at the proxy";
diff --git a/nixos/modules/services/web-apps/bookstack.nix b/nixos/modules/services/web-apps/bookstack.nix
index 3fbccf5400879..40bb377e2c88a 100644
--- a/nixos/modules/services/web-apps/bookstack.nix
+++ b/nixos/modules/services/web-apps/bookstack.nix
@@ -60,11 +60,8 @@ in {
 
     hostname = lib.mkOption {
       type = lib.types.str;
-      default = if config.networking.domain != null then
-                  config.networking.fqdn
-                else
-                  config.networking.hostName;
-      defaultText = lib.literalExpression "config.networking.fqdn";
+      default = config.networking.fqdnOrHostName;
+      defaultText = lib.literalExpression "config.networking.fqdnOrHostName";
       example = "bookstack.example.com";
       description = lib.mdDoc ''
         The hostname to serve BookStack on.
@@ -372,7 +369,7 @@ in {
         User = user;
         WorkingDirectory = "${bookstack}";
         RuntimeDirectory = "bookstack/cache";
-        RuntimeDirectoryMode = 0700;
+        RuntimeDirectoryMode = "0700";
       };
       path = [ pkgs.replace-secret ];
       script =
diff --git a/nixos/modules/services/web-apps/changedetection-io.nix b/nixos/modules/services/web-apps/changedetection-io.nix
new file mode 100644
index 0000000000000..ace4cf1eabc92
--- /dev/null
+++ b/nixos/modules/services/web-apps/changedetection-io.nix
@@ -0,0 +1,219 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.changedetection-io;
+in
+{
+  options.services.changedetection-io = {
+    enable = mkEnableOption (lib.mdDoc "changedetection-io");
+
+    user = mkOption {
+      default = "changedetection-io";
+      type = types.str;
+      description = lib.mdDoc ''
+        User account under which changedetection-io runs.
+      '';
+    };
+
+    group = mkOption {
+      default = "changedetection-io";
+      type = types.str;
+      description = lib.mdDoc ''
+        Group account under which changedetection-io runs.
+      '';
+    };
+
+    listenAddress = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = lib.mdDoc "Address the server will listen on.";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5000;
+      description = lib.mdDoc "Port the server will listen on.";
+    };
+
+    datastorePath = mkOption {
+      type = types.str;
+      default = "/var/lib/changedetection-io";
+      description = lib.mdDoc ''
+        The directory used to store all data for changedetection-io.
+      '';
+    };
+
+    baseURL = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "https://changedetection-io.example";
+      description = lib.mdDoc ''
+        The base url used in notifications and `{base_url}` token.
+      '';
+    };
+
+    behindProxy = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Enable this option when changedetection-io runs behind a reverse proxy, so that it trusts X-* headers.
+        It is recommend to run changedetection-io behind a TLS reverse proxy.
+      '';
+    };
+
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/run/secrets/changedetection-io.env";
+      description = lib.mdDoc ''
+        Securely pass environment variabels to changedetection-io.
+
+        This can be used to set for example a frontend password reproducible via `SALTED_PASS`
+        which convinetly also deactivates nags about the hosted version.
+        `SALTED_PASS` should be 64 characters long while the first 32 are the salt and the second the frontend password.
+        It can easily be retrieved from the settings file when first set via the frontend with the following command:
+        ``jq -r .settings.application.password /var/lib/changedetection-io/url-watches.json``
+      '';
+    };
+
+    webDriverSupport = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Enable support for fetching web pages using WebDriver and Chromium.
+        This starts a headless chromium controlled by puppeteer in an oci container.
+
+        ::: {.note}
+        Playwright can currently leak memory.
+        See https://github.com/dgtlmoon/changedetection.io/wiki/Playwright-content-fetcher#playwright-memory-leak
+        :::
+      '';
+    };
+
+    playwrightSupport = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Enable support for fetching web pages using playwright and Chromium.
+        This starts a headless Chromium controlled by puppeteer in an oci container.
+
+        ::: {.note}
+        Playwright can currently leak memory.
+        See https://github.com/dgtlmoon/changedetection.io/wiki/Playwright-content-fetcher#playwright-memory-leak
+        :::
+      '';
+    };
+
+    chromePort = mkOption {
+      type = types.port;
+      default = 4444;
+      description = lib.mdDoc ''
+        A free port on which webDriverSupport or playwrightSupport listen on localhost.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !((cfg.webDriverSupport == true) && (cfg.playwrightSupport == true));
+        message = "'services.changedetection-io.webDriverSupport' and 'services.changedetection-io.playwrightSupport' cannot be used together.";
+      }
+    ];
+
+    systemd = let
+      defaultStateDir = cfg.datastorePath == "/var/lib/changedetection-io";
+    in {
+      services.changedetection-io = {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        preStart = ''
+          mkdir -p ${cfg.datastorePath}
+        '';
+        serviceConfig = {
+          User = cfg.user;
+          Group = cfg.group;
+          StateDirectory = mkIf defaultStateDir "changedetection-io";
+          StateDirectoryMode = mkIf defaultStateDir "0750";
+          WorkingDirectory = cfg.datastorePath;
+          Environment = lib.optional (cfg.baseURL != null) "BASE_URL=${cfg.baseURL}"
+            ++ lib.optional cfg.behindProxy "USE_X_SETTINGS=1"
+            ++ lib.optional cfg.webDriverSupport "WEBDRIVER_URL=http://127.0.0.1:${toString cfg.chromePort}/wd/hub"
+            ++ lib.optional cfg.playwrightSupport "PLAYWRIGHT_DRIVER_URL=ws://127.0.0.1:${toString cfg.chromePort}/?stealth=1&--disable-web-security=true";
+          EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
+          ExecStart = ''
+            ${pkgs.changedetection-io}/bin/changedetection.py \
+              -h ${cfg.listenAddress} -p ${toString cfg.port} -d ${cfg.datastorePath}
+          '';
+          ProtectHome = true;
+          ProtectSystem = true;
+          Restart = "on-failure";
+        };
+      };
+      tmpfiles.rules = mkIf defaultStateDir [
+        "d ${cfg.datastorePath} 0750 ${cfg.user} ${cfg.group} - -"
+      ];
+    };
+
+    users = {
+      users = optionalAttrs (cfg.user == "changedetection-io") {
+        "changedetection-io" = {
+          isSystemUser = true;
+          group = "changedetection-io";
+        };
+      };
+
+      groups = optionalAttrs (cfg.group == "changedetection-io") {
+        "changedetection-io" = { };
+      };
+    };
+
+    virtualisation = {
+      oci-containers.containers = lib.mkMerge [
+        (mkIf cfg.webDriverSupport {
+          changedetection-io-webdriver = {
+            image = "selenium/standalone-chrome";
+            environment = {
+              VNC_NO_PASSWORD = "1";
+              SCREEN_WIDTH = "1920";
+              SCREEN_HEIGHT = "1080";
+              SCREEN_DEPTH = "24";
+            };
+            ports = [
+              "127.0.0.1:${toString cfg.chromePort}:4444"
+            ];
+            volumes = [
+              "/dev/shm:/dev/shm"
+            ];
+            extraOptions = [ "--network=bridge" ];
+          };
+        })
+
+        (mkIf cfg.playwrightSupport {
+          changedetection-io-playwright = {
+            image = "browserless/chrome";
+            environment = {
+              SCREEN_WIDTH = "1920";
+              SCREEN_HEIGHT = "1024";
+              SCREEN_DEPTH = "16";
+              ENABLE_DEBUGGER = "false";
+              PREBOOT_CHROME = "true";
+              CONNECTION_TIMEOUT = "300000";
+              MAX_CONCURRENT_SESSIONS = "10";
+              CHROME_REFRESH_TIME = "600000";
+              DEFAULT_BLOCK_ADS = "true";
+              DEFAULT_STEALTH = "true";
+            };
+            ports = [
+              "127.0.0.1:${toString cfg.chromePort}:3000"
+            ];
+            extraOptions = [ "--network=bridge" ];
+          };
+        })
+      ];
+      podman.defaultNetwork.dnsname.enable = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/dex.nix b/nixos/modules/services/web-apps/dex.nix
index a171487d4f248..1dcc6f7a7c5bc 100644
--- a/nixos/modules/services/web-apps/dex.nix
+++ b/nixos/modules/services/web-apps/dex.nix
@@ -58,7 +58,7 @@ in
       '';
       description = lib.mdDoc ''
         The available options can be found in
-        [the example configuration](https://github.com/dexidp/dex/blob/v${pkgs.dex.version}/config.yaml.dist).
+        [the example configuration](https://github.com/dexidp/dex/blob/v${pkgs.dex-oidc.version}/config.yaml.dist).
 
         It's also possible to refer to environment variables (defined in [services.dex.environmentFile](#opt-services.dex.environmentFile))
         using the syntax `$VARIABLE_NAME`.
@@ -119,7 +119,7 @@ in
         RestrictRealtime = true;
         RestrictSUIDSGID = true;
         SystemCallArchitectures = "native";
-        SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
+        SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ];
         TemporaryFileSystem = "/:ro";
         # Does not work well with the temporary root
         #UMask = "0066";
diff --git a/nixos/modules/services/web-apps/discourse.nix b/nixos/modules/services/web-apps/discourse.nix
index 66b22ec87db12..6500b8cad217b 100644
--- a/nixos/modules/services/web-apps/discourse.nix
+++ b/nixos/modules/services/web-apps/discourse.nix
@@ -42,11 +42,8 @@ in
 
       hostname = lib.mkOption {
         type = lib.types.str;
-        default = if config.networking.domain != null then
-                    config.networking.fqdn
-                  else
-                    config.networking.hostName;
-        defaultText = lib.literalExpression "config.networking.fqdn";
+        default = config.networking.fqdnOrHostName;
+        defaultText = lib.literalExpression "config.networking.fqdnOrHostName";
         example = "discourse.example.com";
         description = lib.mdDoc ''
           The hostname to serve Discourse on.
@@ -798,13 +795,13 @@ in
           "public"
           "sockets"
         ];
-        RuntimeDirectoryMode = 0750;
+        RuntimeDirectoryMode = "0750";
         StateDirectory = map (p: "discourse/" + p) [
           "uploads"
           "backups"
           "tmp"
         ];
-        StateDirectoryMode = 0750;
+        StateDirectoryMode = "0750";
         LogsDirectory = "discourse";
         TimeoutSec = "infinity";
         Restart = "on-failure";
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
index f4016cd5228e3..f0b3c7b2bcf89 100644
--- a/nixos/modules/services/web-apps/dokuwiki.nix
+++ b/nixos/modules/services/web-apps/dokuwiki.nix
@@ -7,7 +7,6 @@ let
   eachSite = cfg.sites;
   user = "dokuwiki";
   webserver = config.services.${cfg.webserver};
-  stateDir = hostName: "/var/lib/dokuwiki/${hostName}/data";
 
   dokuwikiAclAuthConfig = hostName: cfg: pkgs.writeText "acl.auth-${hostName}.php" ''
     # acl.auth.php
@@ -325,17 +324,17 @@ in
 
   {
     systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
-      "d ${stateDir hostName}/attic 0750 ${user} ${webserver.group} - -"
-      "d ${stateDir hostName}/cache 0750 ${user} ${webserver.group} - -"
-      "d ${stateDir hostName}/index 0750 ${user} ${webserver.group} - -"
-      "d ${stateDir hostName}/locks 0750 ${user} ${webserver.group} - -"
-      "d ${stateDir hostName}/log 0750 ${user} ${webserver.group} - -"
-      "d ${stateDir hostName}/media 0750 ${user} ${webserver.group} - -"
-      "d ${stateDir hostName}/media_attic 0750 ${user} ${webserver.group} - -"
-      "d ${stateDir hostName}/media_meta 0750 ${user} ${webserver.group} - -"
-      "d ${stateDir hostName}/meta 0750 ${user} ${webserver.group} - -"
-      "d ${stateDir hostName}/pages 0750 ${user} ${webserver.group} - -"
-      "d ${stateDir hostName}/tmp 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/attic 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/cache 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/index 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/locks 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/log 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/media 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/media_attic 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/media_meta 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/meta 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/pages 0750 ${user} ${webserver.group} - -"
+      "d ${cfg.stateDir}/tmp 0750 ${user} ${webserver.group} - -"
     ] ++ lib.optional (cfg.aclFile != null) "C ${cfg.aclFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/acl.auth.php.dist"
     ++ lib.optional (cfg.usersFile != null) "C ${cfg.usersFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/users.auth.php.dist"
     ) eachSite);
@@ -359,7 +358,7 @@ in
           };
 
           "~ ^/data/" = {
-            root = "${stateDir hostName}";
+            root = "${cfg.stateDir}";
             extraConfig = "internal;";
           };
 
diff --git a/nixos/modules/services/web-apps/freshrss.nix b/nixos/modules/services/web-apps/freshrss.nix
index 68780a3fe9dd1..c05e7b2c4f7f7 100644
--- a/nixos/modules/services/web-apps/freshrss.nix
+++ b/nixos/modules/services/web-apps/freshrss.nix
@@ -155,9 +155,17 @@ in
         virtualHosts.${cfg.virtualHost} = {
           root = "${cfg.package}/p";
 
+          # php files handling
+          # this regex is mandatory because of the API
           locations."~ ^.+?\.php(/.*)?$".extraConfig = ''
             fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
             fastcgi_split_path_info ^(.+\.php)(/.*)$;
+            # By default, the variable PATH_INFO is not set under PHP-FPM
+            # But FreshRSS API greader.php need it. If you have a “Bad Request” error, double check this var!
+            # NOTE: the separate $path_info variable is required. For more details, see:
+            # https://trac.nginx.org/nginx/ticket/321
+            set $path_info $fastcgi_path_info;
+            fastcgi_param PATH_INFO $path_info;
             include ${pkgs.nginx}/conf/fastcgi_params;
             include ${pkgs.nginx}/conf/fastcgi.conf;
           '';
@@ -238,17 +246,17 @@ in
             # do installation or reconfigure
             if test -f ${cfg.dataDir}/config.php; then
               # reconfigure with settings
-              ${pkgs.php}/bin/php ./cli/reconfigure.php ${settingsFlags}
-              ${pkgs.php}/bin/php ./cli/update-user.php --user ${cfg.defaultUser} --password "$(cat ${cfg.passwordFile})"
+              ./cli/reconfigure.php ${settingsFlags}
+              ./cli/update-user.php --user ${cfg.defaultUser} --password "$(cat ${cfg.passwordFile})"
             else
               # Copy the user data template directory
               cp -r ./data ${cfg.dataDir}
 
               # check correct folders in data folder
-              ${pkgs.php}/bin/php ./cli/prepare.php
+              ./cli/prepare.php
               # install with settings
-              ${pkgs.php}/bin/php ./cli/do-install.php ${settingsFlags}
-              ${pkgs.php}/bin/php ./cli/create-user.php --user ${cfg.defaultUser} --password "$(cat ${cfg.passwordFile})"
+              ./cli/do-install.php ${settingsFlags}
+              ./cli/create-user.php --user ${cfg.defaultUser} --password "$(cat ${cfg.passwordFile})"
             fi
           '';
         };
@@ -267,7 +275,7 @@ in
           Group = "freshrss";
           StateDirectory = "freshrss";
           WorkingDirectory = cfg.package;
-          ExecStart = "${pkgs.php}/bin/php ./app/actualize_script.php";
+          ExecStart = "${cfg.package}/app/actualize_script.php";
         } // systemd-hardening;
       };
     };
diff --git a/nixos/modules/services/web-apps/galene.nix b/nixos/modules/services/web-apps/galene.nix
index ded104792bc08..15ef09aa0b879 100644
--- a/nixos/modules/services/web-apps/galene.nix
+++ b/nixos/modules/services/web-apps/galene.nix
@@ -191,7 +191,7 @@ in
           RestrictRealtime = true;
           RestrictSUIDSGID = true;
           SystemCallArchitectures = "native";
-          SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+          SystemCallFilter = [ "@system-service" "~@privileged" ];
           UMask = "0077";
         }
       ];
diff --git a/nixos/modules/services/web-apps/healthchecks.nix b/nixos/modules/services/web-apps/healthchecks.nix
index 2c55f5ec8ebb5..7da6dce1f9543 100644
--- a/nixos/modules/services/web-apps/healthchecks.nix
+++ b/nixos/modules/services/web-apps/healthchecks.nix
@@ -15,14 +15,14 @@ let
 
   environmentFile = pkgs.writeText "healthchecks-environment" (lib.generators.toKeyValue { } environment);
 
-  healthchecksManageScript = with pkgs; (writeShellScriptBin "healthchecks-manage" ''
+  healthchecksManageScript = pkgs.writeShellScriptBin "healthchecks-manage" ''
+    sudo=exec
     if [[ "$USER" != "${cfg.user}" ]]; then
-        echo "please run as user 'healtchecks'." >/dev/stderr
-        exit 1
+      sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} --preserve-env --preserve-env=PYTHONPATH'
     fi
-    export $(cat ${environmentFile} | xargs);
-    exec ${pkg}/opt/healthchecks/manage.py "$@"
-  '');
+    export $(cat ${environmentFile} | xargs)
+    $sudo ${pkg}/opt/healthchecks/manage.py "$@"
+  '';
 in
 {
   options.services.healthchecks = {
@@ -163,7 +163,7 @@ in
           WorkingDirectory = cfg.dataDir;
           User = cfg.user;
           Group = cfg.group;
-          EnvironmentFile = environmentFile;
+          EnvironmentFile = [ environmentFile ];
           StateDirectory = mkIf (cfg.dataDir == "/var/lib/healthchecks") "healthchecks";
           StateDirectoryMode = mkIf (cfg.dataDir == "/var/lib/healthchecks") "0750";
         };
diff --git a/nixos/modules/services/web-apps/invoiceplane.nix b/nixos/modules/services/web-apps/invoiceplane.nix
index 156cc238e89af..cccf70295e898 100644
--- a/nixos/modules/services/web-apps/invoiceplane.nix
+++ b/nixos/modules/services/web-apps/invoiceplane.nix
@@ -25,6 +25,7 @@ let
     ENCRYPTION_KEY=
     ENCRYPTION_CIPHER=AES-256
     SETUP_COMPLETED=false
+    REMOVE_INDEXPHP=true
   '';
 
   extraConfig = hostName: cfg: pkgs.writeText "extraConfig.php" ''
@@ -36,10 +37,10 @@ let
     version = src.version;
     src = pkgs.invoiceplane;
 
-    patchPhase = ''
+    postPhase = ''
       # Patch index.php file to load additional config file
       substituteInPlace index.php \
-        --replace "require('vendor/autoload.php');" "require('vendor/autoload.php'); \$dotenv = new \Dotenv\Dotenv(__DIR__, 'extraConfig.php'); \$dotenv->load();";
+        --replace "require('vendor/autoload.php');" "require('vendor/autoload.php'); \$dotenv = Dotenv\Dotenv::createImmutable(__DIR__, 'extraConfig.php'); \$dotenv->load();";
     '';
 
     installPhase = ''
@@ -184,6 +185,26 @@ let
           '';
         };
 
+        cron = {
+
+          enable = mkOption {
+            type = types.bool;
+            default = false;
+            description = lib.mdDoc ''
+              Enable cron service which periodically runs Invoiceplane tasks.
+              Requires key taken from the administration page. Refer to
+              <https://wiki.invoiceplane.com/en/1.0/modules/recurring-invoices>
+              on how to configure it.
+            '';
+          };
+
+          key = mkOption {
+            type = types.str;
+            description = lib.mdDoc "Cron key taken from the administration page.";
+          };
+
+        };
+
       };
 
     };
@@ -224,8 +245,11 @@ in
       }
       { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
         message = ''services.invoiceplane.sites."${hostName}".database.passwordFile cannot be specified if services.invoiceplane.sites."${hostName}".database.createLocally is set to true.'';
-      }]
-    ) eachSite);
+      }
+      { assertion = cfg.cron.enable -> cfg.cron.key != null;
+        message = ''services.invoiceplane.sites."${hostName}".cron.key must be set in order to use cron service.'';
+      }
+    ]) eachSite);
 
     services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
       enable = true;
@@ -255,6 +279,7 @@ in
   }
 
   {
+
     systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
       "d ${cfg.stateDir} 0750 ${user} ${webserver.group} - -"
       "f ${cfg.stateDir}/ipconfig.php 0750 ${user} ${webserver.group} - -"
@@ -284,6 +309,34 @@ in
       group = webserver.group;
       isSystemUser = true;
     };
+
+  }
+  {
+
+    # Cron service implementation
+
+    systemd.timers = mapAttrs' (hostName: cfg: (
+      nameValuePair "invoiceplane-cron-${hostName}" (mkIf cfg.cron.enable {
+        wantedBy = [ "timers.target" ];
+        timerConfig = {
+          OnBootSec = "5m";
+          OnUnitActiveSec = "5m";
+          Unit = "invoiceplane-cron-${hostName}.service";
+        };
+      })
+    )) eachSite;
+
+    systemd.services =
+      (mapAttrs' (hostName: cfg: (
+        nameValuePair "invoiceplane-cron-${hostName}" (mkIf cfg.cron.enable {
+          serviceConfig = {
+            Type = "oneshot";
+            User = user;
+            ExecStart = "${pkgs.curl}/bin/curl --header 'Host: ${hostName}' http://localhost/invoices/cron/recur/${cfg.cron.key}";
+          };
+        })
+    )) eachSite);
+
   }
 
   (mkIf (cfg.webserver == "caddy") {
@@ -292,9 +345,8 @@ in
       virtualHosts = mapAttrs' (hostName: cfg: (
         nameValuePair "http://${hostName}" {
           extraConfig = ''
-            root    * ${pkg hostName cfg}
+            root * ${pkg hostName cfg}
             file_server
-
             php_fastcgi unix/${config.services.phpfpm.pools."invoiceplane-${hostName}".socket}
           '';
         }
@@ -302,6 +354,5 @@ in
     };
   })
 
-
   ]);
 }
diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
index da53d4ea76f40..521cf778a36bf 100644
--- a/nixos/modules/services/web-apps/keycloak.nix
+++ b/nixos/modules/services/web-apps/keycloak.nix
@@ -616,7 +616,7 @@ in
               Group = "keycloak";
               DynamicUser = true;
               RuntimeDirectory = "keycloak";
-              RuntimeDirectoryMode = 0700;
+              RuntimeDirectoryMode = "0700";
               AmbientCapabilities = "CAP_NET_BIND_SERVICE";
             };
             script = ''
diff --git a/nixos/modules/services/web-apps/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix
index 779e7d4ad44bf..d159d2ade0630 100644
--- a/nixos/modules/services/web-apps/mastodon.nix
+++ b/nixos/modules/services/web-apps/mastodon.nix
@@ -425,6 +425,39 @@ in {
           Do automatic database migrations.
         '';
       };
+
+      mediaAutoRemove = {
+        enable = lib.mkOption {
+          type = lib.types.bool;
+          default = true;
+          example = false;
+          description = lib.mdDoc ''
+            Automatically remove remote media attachments and preview cards older than the configured amount of days.
+
+            Recommended in https://docs.joinmastodon.org/admin/setup/.
+          '';
+        };
+
+        startAt = lib.mkOption {
+          type = lib.types.str;
+          default = "daily";
+          example = "hourly";
+          description = lib.mdDoc ''
+            How often to remove remote media.
+
+            The format is described in {manpage}`systemd.time(7)`.
+          '';
+        };
+
+        olderThanDays = lib.mkOption {
+          type = lib.types.int;
+          default = 30;
+          example = 14;
+          description = lib.mdDoc ''
+            How old remote media needs to be in order to be removed.
+          '';
+        };
+      };
     };
   };
 
@@ -475,7 +508,6 @@ in {
       } // cfgService;
 
       after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
     };
 
     systemd.services.mastodon-init-db = lib.mkIf cfg.automaticMigrations {
@@ -500,16 +532,21 @@ in {
         # System Call Filtering
         SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
       } // cfgService;
-      after = [ "mastodon-init-dirs.service" "network.target" ] ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else []);
-      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "mastodon-init-dirs.service" ]
+        ++ lib.optional databaseActuallyCreateLocally "postgresql.service";
+      requires = [ "mastodon-init-dirs.service" ]
+        ++ lib.optional databaseActuallyCreateLocally "postgresql.service";
     };
 
     systemd.services.mastodon-streaming = {
-      after = [ "network.target" ]
-        ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
-        ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
-      description = "Mastodon streaming";
+      after = [ "network.target" "mastodon-init-dirs.service" ]
+        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
+        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
+      requires = [ "mastodon-init-dirs.service" ]
+        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
+        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
       wantedBy = [ "multi-user.target" ];
+      description = "Mastodon streaming";
       environment = env // (if cfg.enableUnixSocket
         then { SOCKET = "/run/mastodon-streaming/streaming.socket"; }
         else { PORT = toString(cfg.streamingPort); }
@@ -529,11 +566,14 @@ in {
     };
 
     systemd.services.mastodon-web = {
-      after = [ "network.target" ]
-        ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
-        ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
-      description = "Mastodon web";
+      after = [ "network.target" "mastodon-init-dirs.service" ]
+        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
+        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
+      requires = [ "mastodon-init-dirs.service" ]
+        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
+        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
       wantedBy = [ "multi-user.target" ];
+      description = "Mastodon web";
       environment = env // (if cfg.enableUnixSocket
         then { SOCKET = "/run/mastodon-web/web.socket"; }
         else { PORT = toString(cfg.webPort); }
@@ -554,11 +594,14 @@ in {
     };
 
     systemd.services.mastodon-sidekiq = {
-      after = [ "network.target" ]
-        ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
-        ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
-      description = "Mastodon sidekiq";
+      after = [ "network.target" "mastodon-init-dirs.service" ]
+        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
+        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
+      requires = [ "mastodon-init-dirs.service" ]
+        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
+        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
       wantedBy = [ "multi-user.target" ];
+      description = "Mastodon sidekiq";
       environment = env // {
         PORT = toString(cfg.sidekiqPort);
         DB_POOL = toString cfg.sidekiqThreads;
@@ -575,6 +618,22 @@ in {
       path = with pkgs; [ file imagemagick ffmpeg ];
     };
 
+    systemd.services.mastodon-media-auto-remove = lib.mkIf cfg.mediaAutoRemove.enable {
+      description = "Mastodon media auto remove";
+      environment = env;
+      serviceConfig = {
+        Type = "oneshot";
+        EnvironmentFile = "/var/lib/mastodon/.secrets_env";
+      } // cfgService;
+      script = let
+        olderThanDays = toString cfg.mediaAutoRemove.olderThanDays;
+      in ''
+        ${cfg.package}/bin/tootctl media remove --days=${olderThanDays}
+        ${cfg.package}/bin/tootctl preview_cards remove --days=${olderThanDays}
+      '';
+      startAt = cfg.mediaAutoRemove.startAt;
+    };
+
     services.nginx = lib.mkIf cfg.configureNginx {
       enable = true;
       recommendedProxySettings = true; # required for redirections to work
diff --git a/nixos/modules/services/web-apps/matomo.nix b/nixos/modules/services/web-apps/matomo.nix
index 9e8d85161da76..117d540ba36b1 100644
--- a/nixos/modules/services/web-apps/matomo.nix
+++ b/nixos/modules/services/web-apps/matomo.nix
@@ -12,8 +12,6 @@ let
   phpExecutionUnit = "phpfpm-${pool}";
   databaseService = "mysql.service";
 
-  fqdn = if config.networking.domain != null then config.networking.fqdn else config.networking.hostName;
-
 in {
   imports = [
     (mkRenamedOptionModule [ "services" "piwik" "enable" ] [ "services" "matomo" "enable" ])
@@ -77,11 +75,9 @@ in {
 
       hostname = mkOption {
         type = types.str;
-        default = "${user}.${fqdn}";
+        default = "${user}.${config.networking.fqdnOrHostName}";
         defaultText = literalExpression ''
-          if config.${options.networking.domain} != null
-          then "${user}.''${config.${options.networking.fqdn}}"
-          else "${user}.''${config.${options.networking.hostName}}"
+          "${user}.''${config.${options.networking.fqdnOrHostName}}"
         '';
         example = "matomo.yourdomain.org";
         description = lib.mdDoc ''
diff --git a/nixos/modules/services/web-apps/miniflux.nix b/nixos/modules/services/web-apps/miniflux.nix
index fad5701aeedc4..34a108cebd2b8 100644
--- a/nixos/modules/services/web-apps/miniflux.nix
+++ b/nixos/modules/services/web-apps/miniflux.nix
@@ -116,7 +116,7 @@ in
         RestrictRealtime = true;
         RestrictSUIDSGID = true;
         SystemCallArchitectures = "native";
-        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
         UMask = "0077";
       };
 
diff --git a/nixos/modules/services/web-apps/netbox.nix b/nixos/modules/services/web-apps/netbox.nix
index 2826e57f2c776..f09a8dfc5b215 100644
--- a/nixos/modules/services/web-apps/netbox.nix
+++ b/nixos/modules/services/web-apps/netbox.nix
@@ -46,7 +46,7 @@ let
     '';
   })).override {
     plugins = ps: ((cfg.plugins ps)
-      ++ optional cfg.enableLdap [ ps.django-auth-ldap ]);
+      ++ optionals cfg.enableLdap [ ps.django-auth-ldap ]);
   };
   netboxManageScript = with pkgs; (writeScriptBin "netbox-manage" ''
     #!${stdenv.shell}
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index fdbaa90ebb7a3..04599884f139c 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -148,6 +148,15 @@ in {
       default = 2;
       description = lib.mdDoc "Log level value between 0 (DEBUG) and 4 (FATAL).";
     };
+    logType = mkOption {
+      type = types.enum [ "errorlog" "file" "syslog" "systemd" ];
+      default = "syslog";
+      description = lib.mdDoc ''
+        Logging backend to use.
+        systemd requires the php-systemd package to be added to services.nextcloud.phpExtraExtensions.
+        See the [nextcloud documentation](https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/logging_configuration.html) for details.
+      '';
+    };
     https = mkOption {
       type = types.bool;
       default = false;
@@ -156,7 +165,7 @@ in {
     package = mkOption {
       type = types.package;
       description = lib.mdDoc "Which package to use for the Nextcloud instance.";
-      relatedPackages = [ "nextcloud23" "nextcloud24" ];
+      relatedPackages = [ "nextcloud24" "nextcloud25" ];
     };
     phpPackage = mkOption {
       type = types.package;
@@ -604,7 +613,7 @@ in {
 
   config = mkIf cfg.enable (mkMerge [
     { warnings = let
-        latest = 24;
+        latest = 25;
         upgradeWarning = major: nixos:
           ''
             A legacy Nextcloud install (from before NixOS ${nixos}) may be installed.
@@ -637,10 +646,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 "21") (upgradeWarning 20 "21.05"))
-        ++ (optional (versionOlder cfg.package.version "22") (upgradeWarning 21 "21.11"))
         ++ (optional (versionOlder cfg.package.version "23") (upgradeWarning 22 "22.05"))
         ++ (optional (versionOlder cfg.package.version "24") (upgradeWarning 23 "22.05"))
+        ++ (optional (versionOlder cfg.package.version "25") (upgradeWarning 24 "22.11"))
         ++ (optional isUnsupportedMariadb ''
             You seem to be using MariaDB at an unsupported version (i.e. at least 10.6)!
             Please note that this isn't supported officially by Nextcloud. You can either
@@ -661,19 +669,13 @@ in {
               nextcloud defined in an overlay, please set `services.nextcloud.package` to
               `pkgs.nextcloud`.
             ''
-          else if versionOlder stateVersion "22.05" then nextcloud22
-          else nextcloud24
+          else if versionOlder stateVersion "22.11" then nextcloud24
+          else nextcloud25
         );
 
       services.nextcloud.phpPackage =
         if versionOlder cfg.package.version "24" then pkgs.php80
-        # FIXME: Use PHP 8.1 with Nextcloud 24 and higher, once issues like this one are fixed:
-        #
-        # https://github.com/nextcloud/twofactor_totp/issues/1192
-        #
-        # else if versionOlder cfg.package.version "24" then pkgs.php80
-        # else pkgs.php81;
-        else pkgs.php80;
+        else pkgs.php81;
     }
 
     { assertions = [
@@ -765,7 +767,7 @@ in {
               'datadirectory' => '${datadir}/data',
               'skeletondirectory' => '${cfg.skeletonDirectory}',
               ${optionalString cfg.caching.apcu "'memcache.local' => '\\OC\\Memcache\\APCu',"}
-              'log_type' => 'syslog',
+              'log_type' => '${cfg.logType}',
               'loglevel' => '${builtins.toString cfg.logLevel}',
               ${optionalString (c.overwriteProtocol != null) "'overwriteprotocol' => '${c.overwriteProtocol}',"}
               ${optionalString (c.dbname != null) "'dbname' => '${c.dbname}',"}
@@ -821,9 +823,9 @@ in {
               ${if c.dbhost != null then "--database-host" else null} = ''"${c.dbhost}"'';
               ${if c.dbport != null then "--database-port" else null} = ''"${toString c.dbport}"'';
               ${if c.dbuser != null then "--database-user" else null} = ''"${c.dbuser}"'';
-              "--database-pass" = "\$${dbpass.arg}";
+              "--database-pass" = "\"\$${dbpass.arg}\"";
               "--admin-user" = ''"${c.adminuser}"'';
-              "--admin-pass" = "\$${adminpass.arg}";
+              "--admin-pass" = "\"\$${adminpass.arg}\"";
               "--data-dir" = ''"${datadir}/data"'';
             });
           in ''
diff --git a/nixos/modules/services/web-apps/nextcloud.xml b/nixos/modules/services/web-apps/nextcloud.xml
index b46f34420a703..a0b69dbd606ce 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>nextcloud24</package> which is also the latest
+  The current default by NixOS is <package>nextcloud25</package> which is also the latest
   major version available.
  </para>
  <section xml:id="module-services-nextcloud-basic-usage">
diff --git a/nixos/modules/services/web-apps/peertube.nix b/nixos/modules/services/web-apps/peertube.nix
index 1ac6c15dace9a..4dbcb09d2ae28 100644
--- a/nixos/modules/services/web-apps/peertube.nix
+++ b/nixos/modules/services/web-apps/peertube.nix
@@ -67,9 +67,19 @@ let
     node ~/dist/server/tools/peertube.js $@
   '';
 
+  nginxCommonHeaders = lib.optionalString cfg.enableWebHttps ''
+    add_header Strict-Transport-Security      'max-age=63072000; includeSubDomains';
+  '' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
+    add_header Alt-Svc                        'h3=":443"; ma=86400';
+  '' + ''
+    add_header Access-Control-Allow-Origin    '*';
+    add_header Access-Control-Allow-Methods   'GET, OPTIONS';
+    add_header Access-Control-Allow-Headers   'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
+  '';
+
 in {
   options.services.peertube = {
-    enable = lib.mkEnableOption (lib.mdDoc "Enable Peertube’s service");
+    enable = lib.mkEnableOption (lib.mdDoc "Peertube");
 
     user = lib.mkOption {
       type = lib.types.str;
@@ -90,13 +100,13 @@ in {
     };
 
     listenHttp = lib.mkOption {
-      type = lib.types.int;
+      type = lib.types.port;
       default = 9000;
       description = lib.mdDoc "listen port for HTTP server.";
     };
 
     listenWeb = lib.mkOption {
-      type = lib.types.int;
+      type = lib.types.port;
       default = 9000;
       description = lib.mdDoc "listen port for WEB server.";
     };
@@ -145,6 +155,12 @@ in {
       description = lib.mdDoc "Configuration for peertube.";
     };
 
+    configureNginx = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = lib.mdDoc "Configure nginx as a reverse proxy for peertube.";
+    };
+
     database = {
       createLocally = lib.mkOption {
         type = lib.types.bool;
@@ -165,7 +181,7 @@ in {
       };
 
       port = lib.mkOption {
-        type = lib.types.int;
+        type = lib.types.port;
         default = 5432;
         description = lib.mdDoc "Database host port.";
       };
@@ -351,12 +367,14 @@ in {
     systemd.tmpfiles.rules = [
       "d '/var/lib/peertube/config' 0700 ${cfg.user} ${cfg.group} - -"
       "z '/var/lib/peertube/config' 0700 ${cfg.user} ${cfg.group} - -"
+      "d '/var/lib/peertube/www' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '/var/lib/peertube/www' 0750 ${cfg.user} ${cfg.group} - -"
     ];
 
     systemd.services.peertube-init-db = lib.mkIf cfg.database.createLocally {
       description = "Initialization database for PeerTube daemon";
       after = [ "network.target" "postgresql.service" ];
-      wantedBy = [ "multi-user.target" ];
+      requires = [ "postgresql.service" ];
 
       script = let
         psqlSetupCommands = pkgs.writeText "peertube-init.sql" ''
@@ -385,7 +403,9 @@ in {
     systemd.services.peertube = {
       description = "PeerTube daemon";
       after = [ "network.target" ]
-        ++ lib.optionals cfg.redis.createLocally [ "redis.service" ]
+        ++ lib.optional cfg.redis.createLocally "redis-peertube.service"
+        ++ lib.optionals cfg.database.createLocally [ "postgresql.service" "peertube-init-db.service" ];
+      requires = lib.optional cfg.redis.createLocally "redis-peertube.service"
         ++ lib.optionals cfg.database.createLocally [ "postgresql.service" "peertube-init-db.service" ];
       wantedBy = [ "multi-user.target" ];
 
@@ -410,8 +430,11 @@ in {
           password: '$(cat ${cfg.smtp.passwordFile})'
         ''}
         EOF
-        ln -sf ${cfg.package}/config/default.yaml /var/lib/peertube/config/default.yaml
+        umask 027
         ln -sf ${configFile} /var/lib/peertube/config/production.json
+        ln -sf ${cfg.package}/config/default.yaml /var/lib/peertube/config/default.yaml
+        ln -sf ${cfg.package}/client/dist -T /var/lib/peertube/www/client
+        ln -sf ${cfg.settings.storage.client_overrides} -T /var/lib/peertube/www/client-overrides
         npm start
       '';
       serviceConfig = {
@@ -441,6 +464,285 @@ in {
       } // cfgService;
     };
 
+    services.nginx = lib.mkIf cfg.configureNginx {
+      enable = true;
+      virtualHosts."${cfg.localDomain}" = {
+        root = "/var/lib/peertube";
+
+        # Application
+        locations."/" = {
+          tryFiles = "/dev/null @api";
+          priority = 1110;
+        };
+
+        locations."= /api/v1/videos/upload-resumable" = {
+          tryFiles = "/dev/null @api";
+          priority = 1120;
+
+          extraConfig = ''
+            client_max_body_size                        0;
+            proxy_request_buffering                     off;
+          '';
+        };
+
+        locations."~ ^/api/v1/videos/(upload|([^/]+/studio/edit))$" = {
+          tryFiles = "/dev/null @api";
+          root = cfg.settings.storage.tmp;
+          priority = 1130;
+
+          extraConfig = ''
+            client_max_body_size                        12G;
+            add_header X-File-Maximum-Size              8G always;
+          '' + lib.optionalString cfg.enableWebHttps ''
+            add_header Strict-Transport-Security        'max-age=63072000; includeSubDomains';
+          '' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
+            add_header Alt-Svc                          'h3=":443"; ma=86400';
+          '';
+        };
+
+        locations."~ ^/api/v1/(videos|video-playlists|video-channels|users/me)" = {
+          tryFiles = "/dev/null @api";
+          priority = 1140;
+
+          extraConfig = ''
+            client_max_body_size                        6M;
+            add_header X-File-Maximum-Size              4M always;
+          '' + lib.optionalString cfg.enableWebHttps ''
+            add_header Strict-Transport-Security        'max-age=63072000; includeSubDomains';
+          '' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
+            add_header Alt-Svc                          'h3=":443"; ma=86400';
+          '';
+        };
+
+        locations."@api" = {
+          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          priority = 1150;
+
+          extraConfig = ''
+            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
+            proxy_set_header Host                       $host;
+            proxy_set_header X-Real-IP                  $remote_addr;
+
+            proxy_connect_timeout                       10m;
+
+            proxy_send_timeout                          10m;
+            proxy_read_timeout                          10m;
+
+            client_max_body_size                        100k;
+            send_timeout                                10m;
+          '';
+        };
+
+        # Websocket
+        locations."/socket.io" = {
+          tryFiles = "/dev/null @api_websocket";
+          priority = 1210;
+        };
+
+        locations."/tracker/socket" = {
+          tryFiles = "/dev/null @api_websocket";
+          priority = 1220;
+
+          extraConfig = ''
+            proxy_read_timeout                          15m;
+          '';
+        };
+
+        locations."@api_websocket" = {
+          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          priority = 1230;
+
+          extraConfig = ''
+            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
+            proxy_set_header Host                       $host;
+            proxy_set_header X-Real-IP                  $remote_addr;
+            proxy_set_header Upgrade                    $http_upgrade;
+            proxy_set_header Connection                 'upgrade';
+
+            proxy_http_version                          1.1;
+          '';
+        };
+
+        # Bypass PeerTube for performance reasons.
+        locations."~ ^/client/(assets/images/(icons/icon-36x36\.png|icons/icon-48x48\.png|icons/icon-72x72\.png|icons/icon-96x96\.png|icons/icon-144x144\.png|icons/icon-192x192\.png|icons/icon-512x512\.png|logo\.svg|favicon\.png|default-playlist\.jpg|default-avatar-account\.png|default-avatar-account-48x48\.png|default-avatar-video-channel\.png|default-avatar-video-channel-48x48\.png))$" = {
+          tryFiles = "/www/client-overrides/$1 /www/client/$1 $1";
+          priority = 1310;
+        };
+
+        locations."~ ^/client/(.*\.(js|css|png|svg|woff2|otf|ttf|woff|eot))$" = {
+          alias = "${cfg.package}/client/dist/$1";
+          priority = 1320;
+          extraConfig = ''
+            add_header Cache-Control                    'public, max-age=604800, immutable';
+          '' + lib.optionalString cfg.enableWebHttps ''
+            add_header Strict-Transport-Security        'max-age=63072000; includeSubDomains';
+          '' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
+            add_header Alt-Svc                          'h3=":443"; ma=86400';
+          '';
+        };
+
+        locations."~ ^/lazy-static/(avatars|banners)/" = {
+          tryFiles = "$uri @api";
+          root = cfg.settings.storage.avatars;
+          priority = 1330;
+          extraConfig = ''
+            if ($request_method = 'OPTIONS') {
+              ${nginxCommonHeaders}
+              add_header Access-Control-Max-Age         1728000;
+              add_header Cache-Control                  'no-cache';
+              add_header Content-Type                   'text/plain charset=UTF-8';
+              add_header Content-Length                 0;
+              return                                    204;
+            }
+
+            ${nginxCommonHeaders}
+            add_header Cache-Control                    'public, max-age=7200';
+
+            rewrite ^/lazy-static/avatars/(.*)$         /$1 break;
+            rewrite ^/lazy-static/banners/(.*)$         /$1 break;
+          '';
+        };
+
+        locations."^~ /lazy-static/previews/" = {
+          tryFiles = "$uri @api";
+          root = cfg.settings.storage.previews;
+          priority = 1340;
+          extraConfig = ''
+            if ($request_method = 'OPTIONS') {
+              ${nginxCommonHeaders}
+              add_header Access-Control-Max-Age         1728000;
+              add_header Cache-Control                  'no-cache';
+              add_header Content-Type                   'text/plain charset=UTF-8';
+              add_header Content-Length                 0;
+              return                                    204;
+            }
+
+            ${nginxCommonHeaders}
+            add_header Cache-Control                    'public, max-age=7200';
+
+            rewrite ^/lazy-static/previews/(.*)$        /$1 break;
+          '';
+        };
+
+        locations."^~ /static/thumbnails/" = {
+          tryFiles = "$uri @api";
+          root = cfg.settings.storage.thumbnails;
+          priority = 1350;
+          extraConfig = ''
+            if ($request_method = 'OPTIONS') {
+              ${nginxCommonHeaders}
+              add_header Access-Control-Max-Age         1728000;
+              add_header Cache-Control                  'no-cache';
+              add_header Content-Type                   'text/plain charset=UTF-8';
+              add_header Content-Length                 0;
+              return                                    204;
+            }
+
+            ${nginxCommonHeaders}
+            add_header Cache-Control                    'public, max-age=7200';
+
+            rewrite ^/static/thumbnails/(.*)$           /$1 break;
+          '';
+        };
+
+        locations."^~ /static/redundancy/" = {
+          tryFiles = "$uri @api";
+          root = cfg.settings.storage.redundancy;
+          priority = 1360;
+          extraConfig = ''
+            if ($request_method = 'OPTIONS') {
+              ${nginxCommonHeaders}
+              add_header Access-Control-Max-Age         1728000;
+              add_header Content-Type                   'text/plain charset=UTF-8';
+              add_header Content-Length                 0;
+              return                                    204;
+            }
+            if ($request_method = 'GET') {
+              ${nginxCommonHeaders}
+
+              access_log                                off;
+            }
+            aio                                         threads;
+            sendfile                                    on;
+            sendfile_max_chunk                          1M;
+
+            limit_rate_after                            5M;
+
+            set $peertube_limit_rate                    800k;
+            set $limit_rate                             $peertube_limit_rate;
+
+            rewrite ^/static/redundancy/(.*)$           /$1 break;
+          '';
+        };
+
+        locations."^~ /static/streaming-playlists/" = {
+          tryFiles = "$uri @api";
+          root = cfg.settings.storage.streaming_playlists;
+          priority = 1370;
+          extraConfig = ''
+            if ($request_method = 'OPTIONS') {
+              ${nginxCommonHeaders}
+              add_header Access-Control-Max-Age         1728000;
+              add_header Content-Type                   'text/plain charset=UTF-8';
+              add_header Content-Length                 0;
+              return                                    204;
+            }
+            if ($request_method = 'GET') {
+              ${nginxCommonHeaders}
+
+              access_log                                off;
+            }
+
+            aio                                         threads;
+            sendfile                                    on;
+            sendfile_max_chunk                          1M;
+
+            limit_rate_after                            5M;
+
+            set $peertube_limit_rate                    5M;
+            set $limit_rate                             $peertube_limit_rate;
+
+            rewrite ^/static/streaming-playlists/(.*)$  /$1 break;
+          '';
+        };
+
+        locations."~ ^/static/webseed/" = {
+          tryFiles = "$uri @api";
+          root = cfg.settings.storage.videos;
+          priority = 1380;
+          extraConfig = ''
+            if ($request_method = 'OPTIONS') {
+              ${nginxCommonHeaders}
+              add_header Access-Control-Max-Age         1728000;
+              add_header Content-Type                   'text/plain charset=UTF-8';
+              add_header Content-Length                 0;
+              return                                    204;
+            }
+            if ($request_method = 'GET') {
+              ${nginxCommonHeaders}
+
+              access_log                                off;
+            }
+
+            aio                                         threads;
+            sendfile                                    on;
+            sendfile_max_chunk                          1M;
+
+            limit_rate_after                            5M;
+
+            set $peertube_limit_rate                    800k;
+            set $limit_rate                             $peertube_limit_rate;
+
+            rewrite ^/static/webseed/(.*)$              /$1 break;
+          '';
+        };
+
+        extraConfig = lib.optionalString cfg.enableWebHttps ''
+          add_header Strict-Transport-Security          'max-age=63072000; includeSubDomains';
+        '';
+      };
+    };
+
     services.postgresql = lib.mkIf cfg.database.createLocally {
       enable = true;
     };
@@ -476,8 +778,10 @@ in {
       (lib.mkIf cfg.redis.enableUnixSocket {${config.services.peertube.user}.extraGroups = [ "redis-peertube" ];})
     ];
 
-    users.groups = lib.optionalAttrs (cfg.group == "peertube") {
-      peertube = { };
+    users.groups = {
+      ${cfg.group} = {
+        members = lib.optional cfg.configureNginx config.services.nginx.user;
+      };
     };
   };
 }
diff --git a/nixos/modules/services/web-apps/prosody-filer.nix b/nixos/modules/services/web-apps/prosody-filer.nix
index 279b4104b0a00..84953546d8e09 100644
--- a/nixos/modules/services/web-apps/prosody-filer.nix
+++ b/nixos/modules/services/web-apps/prosody-filer.nix
@@ -79,7 +79,7 @@ in {
         LockPersonality = true;
         RemoveIPC = true;
         RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
-        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
       };
     };
   };
diff --git a/nixos/modules/services/web-apps/restya-board.nix b/nixos/modules/services/web-apps/restya-board.nix
index 69f9b3ebe5b47..4b32f06826e2c 100644
--- a/nixos/modules/services/web-apps/restya-board.nix
+++ b/nixos/modules/services/web-apps/restya-board.nix
@@ -69,7 +69,7 @@ in
         };
 
         listenPort = mkOption {
-          type = types.int;
+          type = types.port;
           default = 3000;
           description = lib.mdDoc ''
             Listen port for the virtualhost to use.
@@ -132,7 +132,7 @@ in
         };
 
         port = mkOption {
-          type = types.int;
+          type = types.port;
           default = 25;
           description = lib.mdDoc ''
             Port used to connect to SMTP server.
diff --git a/nixos/modules/services/web-apps/shiori.nix b/nixos/modules/services/web-apps/shiori.nix
index 7bd0a4d2b9b5b..f0505e052e1c7 100644
--- a/nixos/modules/services/web-apps/shiori.nix
+++ b/nixos/modules/services/web-apps/shiori.nix
@@ -86,7 +86,7 @@ in {
         SystemCallErrorNumber = "EPERM";
         SystemCallFilter = [
           "@system-service"
-          "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@resources" "~@setuid"
+          "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@setuid"
         ];
       };
     };
diff --git a/nixos/modules/services/web-apps/snipe-it.nix b/nixos/modules/services/web-apps/snipe-it.nix
index 264b72fe837b1..6da44f1bdf34c 100644
--- a/nixos/modules/services/web-apps/snipe-it.nix
+++ b/nixos/modules/services/web-apps/snipe-it.nix
@@ -54,11 +54,8 @@ in {
 
     hostName = lib.mkOption {
       type = lib.types.str;
-      default = if config.networking.domain != null then
-                  config.networking.fqdn
-                else
-                  config.networking.hostName;
-      defaultText = lib.literalExpression "config.networking.fqdn";
+      default = config.networking.fqdnOrHostName;
+      defaultText = lib.literalExpression "config.networking.fqdnOrHostName";
       example = "snipe-it.example.com";
       description = lib.mdDoc ''
         The hostname to serve Snipe-IT on.
@@ -394,7 +391,7 @@ in {
         User = user;
         WorkingDirectory = snipe-it;
         RuntimeDirectory = "snipe-it/cache";
-        RuntimeDirectoryMode = 0700;
+        RuntimeDirectoryMode = "0700";
       };
       path = [ pkgs.replace-secret ];
       script =
@@ -454,25 +451,43 @@ in {
 
           # migrate db
           ${pkgs.php}/bin/php artisan migrate --force
+
+          # A placeholder file for invalid barcodes
+          invalid_barcode_location="${cfg.dataDir}/public/uploads/barcodes/invalid_barcode.gif"
+          [ ! -e "$invalid_barcode_location" ] \
+              && cp ${snipe-it}/share/snipe-it/invalid_barcode.gif "$invalid_barcode_location"
         '';
     };
 
     systemd.tmpfiles.rules = [
-      "d ${cfg.dataDir}                            0710 ${user} ${group} - -"
-      "d ${cfg.dataDir}/bootstrap                  0750 ${user} ${group} - -"
-      "d ${cfg.dataDir}/bootstrap/cache            0750 ${user} ${group} - -"
-      "d ${cfg.dataDir}/public                     0750 ${user} ${group} - -"
-      "d ${cfg.dataDir}/public/uploads             0750 ${user} ${group} - -"
-      "d ${cfg.dataDir}/storage                    0700 ${user} ${group} - -"
-      "d ${cfg.dataDir}/storage/app                0700 ${user} ${group} - -"
-      "d ${cfg.dataDir}/storage/fonts              0700 ${user} ${group} - -"
-      "d ${cfg.dataDir}/storage/framework          0700 ${user} ${group} - -"
-      "d ${cfg.dataDir}/storage/framework/cache    0700 ${user} ${group} - -"
-      "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -"
-      "d ${cfg.dataDir}/storage/framework/views    0700 ${user} ${group} - -"
-      "d ${cfg.dataDir}/storage/logs               0700 ${user} ${group} - -"
-      "d ${cfg.dataDir}/storage/uploads            0700 ${user} ${group} - -"
-      "d ${cfg.dataDir}/storage/private_uploads    0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}                              0710 ${user} ${group} - -"
+      "d ${cfg.dataDir}/bootstrap                    0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/bootstrap/cache              0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public                       0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads               0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/accessories   0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/assets        0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/avatars       0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/barcodes      0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/categories    0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/companies     0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/components    0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/consumables   0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/departments   0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/locations     0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/manufacturers 0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/models        0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads/suppliers     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} - -"
+      "d ${cfg.dataDir}/storage/private_uploads      0700 ${user} ${group} - -"
     ];
 
     users = {
diff --git a/nixos/modules/services/web-apps/trilium.nix b/nixos/modules/services/web-apps/trilium.nix
index 81ed7ca83bc23..a91d64f620b6e 100644
--- a/nixos/modules/services/web-apps/trilium.nix
+++ b/nixos/modules/services/web-apps/trilium.nix
@@ -67,7 +67,7 @@ in
     };
 
     port = mkOption {
-      type = types.int;
+      type = types.port;
       default = 8080;
       description = lib.mdDoc ''
         The port number to bind to.
diff --git a/nixos/modules/services/web-apps/tt-rss.nix b/nixos/modules/services/web-apps/tt-rss.nix
index 870e8f4795b5c..6f494fae4cc18 100644
--- a/nixos/modules/services/web-apps/tt-rss.nix
+++ b/nixos/modules/services/web-apps/tt-rss.nix
@@ -208,7 +208,7 @@ let
         };
 
         port = mkOption {
-          type = types.nullOr types.int;
+          type = types.nullOr types.port;
           default = null;
           description = lib.mdDoc ''
             The database's port. If not set, the default ports will be provided (5432
diff --git a/nixos/modules/services/web-apps/wordpress.nix b/nixos/modules/services/web-apps/wordpress.nix
index 660f1b2d7f868..43a6d7e75dc6c 100644
--- a/nixos/modules/services/web-apps/wordpress.nix
+++ b/nixos/modules/services/web-apps/wordpress.nix
@@ -409,6 +409,8 @@ in
       "d '${stateDir hostName}' 0750 ${user} ${webserver.group} - -"
       "d '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -"
       "Z '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -"
+      "d '${cfg.fontsDir}' 0750 ${user} ${webserver.group} - -"
+      "Z '${cfg.fontsDir}' 0750 ${user} ${webserver.group} - -"
     ]) eachSite);
 
     systemd.services = mkMerge [
diff --git a/nixos/modules/services/web-apps/youtrack.nix b/nixos/modules/services/web-apps/youtrack.nix
index 0db8a98d1eb34..09a2b9e965c0b 100644
--- a/nixos/modules/services/web-apps/youtrack.nix
+++ b/nixos/modules/services/web-apps/youtrack.nix
@@ -68,7 +68,7 @@ in
         The port youtrack will listen on.
       '';
       default = 8080;
-      type = types.int;
+      type = types.port;
     };
 
     statePath = mkOption {
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index 6b43d46fdead0..588f5ee4d003a 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -18,7 +18,7 @@ let
     sed -i $out/bin/apachectl -e 's|$HTTPD -t|$HTTPD -t -f /etc/httpd/httpd.conf|'
   '';
 
-  php = cfg.phpPackage.override { apacheHttpd = pkg; };
+  php = cfg.phpPackage.override { apxs2Support = true; apacheHttpd = pkg; };
 
   phpModuleName = let
     majorVersion = lib.versions.major (lib.getVersion php);
@@ -168,7 +168,7 @@ let
         <VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listen}>
             ServerName ${hostOpts.hostName}
             ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases}
-            ServerAdmin ${adminAddr}
+            ${optionalString (adminAddr != null) "ServerAdmin ${adminAddr}"}
             <IfModule mod_ssl.c>
                 SSLEngine off
             </IfModule>
@@ -187,7 +187,7 @@ let
         <VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listenSSL}>
             ServerName ${hostOpts.hostName}
             ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases}
-            ServerAdmin ${adminAddr}
+            ${optionalString (adminAddr != null) "ServerAdmin ${adminAddr}"}
             SSLEngine on
             SSLCertificateFile ${sslServerCert}
             SSLCertificateKeyFile ${sslServerKey}
@@ -455,8 +455,9 @@ in
       };
 
       adminAddr = mkOption {
-        type = types.str;
+        type = types.nullOr types.str;
         example = "admin@example.org";
+        default = null;
         description = lib.mdDoc "E-mail address of the server administrator.";
       };
 
@@ -659,6 +660,13 @@ in
           `services.httpd.virtualHosts.<name>.useACMEHost` are mutually exclusive.
         '';
       }
+      {
+        assertion = cfg.enablePHP -> php.ztsSupport;
+        message = ''
+          The php package provided by `services.httpd.phpPackage` is not built with zts support. Please
+          ensure the php has zts support by settings `services.httpd.phpPackage = php.override { ztsSupport = true; }`
+        '';
+      }
     ] ++ map (name: mkCertOwnershipAssertion {
       inherit (cfg) group user;
       cert = config.security.acme.certs.${name};
diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix
index e1456091717d4..50213ec252ff2 100644
--- a/nixos/modules/services/web-servers/caddy/default.nix
+++ b/nixos/modules/services/web-servers/caddy/default.nix
@@ -290,6 +290,9 @@ in
       }
     '';
 
+    # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
+    boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
+
     systemd.packages = [ cfg.package ];
     systemd.services.caddy = {
       wants = map (hostOpts: "acme-finished-${hostOpts.useACMEHost}.target") acmeVHosts;
diff --git a/nixos/modules/services/web-servers/garage.nix b/nixos/modules/services/web-servers/garage.nix
new file mode 100644
index 0000000000000..76ab273483eb4
--- /dev/null
+++ b/nixos/modules/services/web-servers/garage.nix
@@ -0,0 +1,91 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.garage;
+  toml = pkgs.formats.toml {};
+  configFile = toml.generate "garage.toml" cfg.settings;
+in
+{
+  meta.maintainers = [ maintainers.raitobezarius ];
+
+  options.services.garage = {
+    enable = mkEnableOption (lib.mdDoc "Garage Object Storage (S3 compatible)");
+
+    extraEnvironment = mkOption {
+      type = types.attrsOf types.str;
+      description = lib.mdDoc "Extra environment variables to pass to the Garage server.";
+      default = {};
+      example = { RUST_BACKTRACE="yes"; };
+    };
+
+    logLevel = mkOption {
+      type = types.enum (["info" "debug" "trace"]);
+      default = "info";
+      example = "debug";
+      description = lib.mdDoc "Garage log level, see <https://garagehq.deuxfleurs.fr/documentation/quick-start/#launching-the-garage-server> for examples.";
+    };
+
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = toml.type;
+
+        options = {
+          metadata_dir = mkOption {
+            default = "/var/lib/garage/meta";
+            type = types.path;
+            description = lib.mdDoc "The metadata directory, put this on a fast disk (e.g. SSD) if possible.";
+          };
+
+          data_dir = mkOption {
+            default = "/var/lib/garage/data";
+            type = types.path;
+            description = lib.mdDoc "The main data storage, put this on your large storage (e.g. high capacity HDD)";
+          };
+
+          replication_mode = mkOption {
+            default = "none";
+            type = types.enum ([ "none" "1" "2" "3" 1 2 3 ]);
+            apply = v: toString v;
+            description = lib.mdDoc "Garage replication mode, defaults to none, see: <https://garagehq.deuxfleurs.fr/reference_manual/configuration.html#replication_mode> for reference.";
+          };
+        };
+      };
+      description = lib.mdDoc "Garage configuration, see <https://garagehq.deuxfleurs.fr/reference_manual/configuration.html> for reference.";
+    };
+
+    package = mkOption {
+      default = pkgs.garage;
+      defaultText = literalExpression "pkgs.garage";
+      type = types.package;
+      description = lib.mdDoc "Garage package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."garage.toml" = {
+      source = configFile;
+    };
+
+    environment.systemPackages = [ cfg.package ]; # For administration
+
+    systemd.services.garage = {
+      description = "Garage Object Storage (S3 compatible)";
+      after = [ "network.target" "network-online.target" ];
+      wants = [ "network.target" "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/garage server";
+
+        StateDirectory = mkIf (hasPrefix "/var/lib/garage" cfg.settings.data_dir && hasPrefix "/var/lib/garage" cfg.settings.metadata_dir) "garage";
+        DynamicUser = lib.mkDefault true;
+        ProtectHome = true;
+        NoNewPrivileges = true;
+      };
+      environment = {
+        RUST_LOG = lib.mkDefault "garage=${cfg.logLevel}";
+      } // cfg.extraEnvironment;
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/merecat.nix b/nixos/modules/services/web-servers/merecat.nix
new file mode 100644
index 0000000000000..aad93605b7176
--- /dev/null
+++ b/nixos/modules/services/web-servers/merecat.nix
@@ -0,0 +1,55 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.merecat;
+  format = pkgs.formats.keyValue {
+    mkKeyValue = generators.mkKeyValueDefault {
+      mkValueString = v:
+        # In merecat.conf, booleans are "true" and "false"
+        if builtins.isBool v
+        then if v then "true" else "false"
+        else generators.mkValueStringDefault {} v;
+    } "=";
+  };
+  configFile = format.generate "merecat.conf" cfg.settings;
+
+in {
+
+  options.services.merecat = {
+
+    enable = mkEnableOption (lib.mdDoc "Merecat HTTP server");
+
+    settings = mkOption {
+      inherit (format) type;
+      default = { };
+      description = lib.mdDoc ''
+        Merecat configuration. Refer to merecat(8) for details on supported values.
+      '';
+      example = {
+        hostname = "localhost";
+        port = 8080;
+        virtual-host = true;
+        directory = "/srv/www";
+      };
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services.merecat = {
+      description = "Merecat HTTP server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.merecat}/bin/merecat -n -f ${configFile}";
+        AmbientCapabilities = lib.mkIf ((cfg.settings.port or 80) < 1024) [ "CAP_NET_BIND_SERVICE" ];
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index aa782b4267e80..12afc23592ba1 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -198,8 +198,8 @@ let
 
       ${optionalString cfg.statusPage ''
         server {
-          listen 80;
-          ${optionalString enableIPv6 "listen [::]:80;" }
+          listen ${toString cfg.defaultHTTPListenPort};
+          ${optionalString enableIPv6 "listen [::]:${toString cfg.defaultHTTPListenPort};" }
 
           server_name localhost;
 
@@ -246,8 +246,8 @@ let
           if vhost.listen != [] then vhost.listen
           else
             let addrs = if vhost.listenAddresses != [] then vhost.listenAddresses else cfg.defaultListenAddresses;
-            in optionals (hasSSL || vhost.rejectSSL) (map (addr: { inherit addr; port = 443; ssl = true; }) addrs)
-              ++ optionals (!onlySSL) (map (addr: { inherit addr; port = 80; ssl = false; }) addrs);
+            in optionals (hasSSL || vhost.rejectSSL) (map (addr: { inherit addr; port = cfg.defaultSSLListenPort; ssl = true; }) addrs)
+              ++ optionals (!onlySSL) (map (addr: { inherit addr; port = cfg.defaultHTTPListenPort; ssl = false; }) addrs);
 
         hostListen =
           if vhost.forceSSL
@@ -275,7 +275,10 @@ let
         redirectListen = filter (x: !x.ssl) defaultListen;
 
         acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) ''
-          location /.well-known/acme-challenge {
+          # Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx)
+          # We use ^~ here, so that we don't check any regexes (which could
+          # otherwise easily override this intended match accidentally).
+          location ^~ /.well-known/acme-challenge/ {
             ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"}
             ${optionalString (vhost.acmeRoot != null) "root ${vhost.acmeRoot};"}
             auth_basic off;
@@ -446,6 +449,24 @@ in
         '';
       };
 
+      defaultHTTPListenPort = mkOption {
+        type = types.port;
+        default = 80;
+        example = 8080;
+        description = lib.mdDoc ''
+          If vhosts do not specify listen.port, use these ports for HTTP by default.
+        '';
+      };
+
+      defaultSSLListenPort = mkOption {
+        type = types.port;
+        default = 443;
+        example = 8443;
+        description = lib.mdDoc ''
+          If vhosts do not specify listen.port, use these ports for SSL by default.
+        '';
+      };
+
       package = mkOption {
         default = pkgs.nginxStable;
         defaultText = literalExpression "pkgs.nginxStable";
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
index ccf8804943ac2..e3d4afc074cfa 100644
--- a/nixos/modules/services/web-servers/nginx/vhost-options.nix
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -88,7 +88,7 @@ with lib;
       type = types.nullOr types.str;
       default = "/var/lib/acme/acme-challenge";
       description = lib.mdDoc ''
-        Directory for the acme challenge which is PUBLIC, don't put certs or keys in here.
+        Directory for the ACME challenge, which is **public**. Don't put certs or keys in here.
         Set to null to inherit from config.security.acme.
       '';
     };
@@ -97,8 +97,12 @@ with lib;
       type = types.nullOr types.str;
       default = null;
       description = lib.mdDoc ''
-        Host which to proxy requests to if acme challenge is not found. Useful
+        Host which to proxy requests to if ACME challenge is not found. Useful
         if you want multiple hosts to be able to verify the same domain name.
+
+        With this option, you could request certificates for the present domain
+        with an ACME client that is running on another host, which you would
+        specify here.
       '';
     };
 
diff --git a/nixos/modules/services/x11/clight.nix b/nixos/modules/services/x11/clight.nix
index 8a17b7e801e54..0f66e191fe281 100644
--- a/nixos/modules/services/x11/clight.nix
+++ b/nixos/modules/services/x11/clight.nix
@@ -28,13 +28,7 @@ let
       cfg.settings));
 in {
   options.services.clight = {
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Whether to enable clight or not.
-      '';
-    };
+    enable = mkEnableOption (lib.mdDoc "clight");
 
     temperature = {
       day = mkOption {
diff --git a/nixos/modules/services/x11/desktop-managers/cinnamon.nix b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
index 3d79a3b8513b8..2c59ee410d5fb 100644
--- a/nixos/modules/services/x11/desktop-managers/cinnamon.nix
+++ b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
@@ -12,6 +12,7 @@ let
     extraGSettingsOverrides = cfg.extraGSettingsOverrides;
   };
 
+  notExcluded = pkg: (!(lib.elem pkg config.environment.cinnamon.excludePackages));
 in
 
 {
@@ -65,10 +66,14 @@ in
         enable = mkDefault true;
 
         # Taken from mint-artwork.gschema.override
-        theme.name = mkDefault "Mint-X";
-        theme.package = mkDefault pkgs.cinnamon.mint-themes;
-        iconTheme.name = mkDefault "Mint-X-Dark";
-        iconTheme.package = mkDefault pkgs.cinnamon.mint-x-icons;
+        theme = mkIf (notExcluded pkgs.cinnamon.mint-themes) {
+          name = mkDefault "Mint-X";
+          package = mkDefault pkgs.cinnamon.mint-themes;
+        };
+        iconTheme = mkIf (notExcluded pkgs.cinnamon.mint-x-icons) {
+          name = mkDefault "Mint-X-Dark";
+          package = mkDefault pkgs.cinnamon.mint-x-icons;
+        };
       };
       services.xserver.displayManager.sessionCommands = ''
         if test "$XDG_CURRENT_DESKTOP" = "Cinnamon"; then
@@ -123,11 +128,8 @@ in
         cinnamon-screensaver = {};
       };
 
-      environment.systemPackages = with pkgs.cinnamon // pkgs; [
+      environment.systemPackages = with pkgs.cinnamon // pkgs; ([
         desktop-file-utils
-        nixos-artwork.wallpapers.simple-dark-gray
-        onboard
-        sound-theme-freedesktop
 
         # common-files
         cinnamon-common
@@ -152,24 +154,32 @@ in
         cinnamon-control-center
         cinnamon-settings-daemon
         libgnomekbd
-        orca
 
         # theme
         gnome.adwaita-icon-theme
-        hicolor-icon-theme
         gnome.gnome-themes-extra
         gtk3.out
+
+        # other
+        glib # for gsettings
+        xdg-user-dirs
+      ] ++ utils.removePackagesByName [
+        # accessibility
+        onboard
+        orca
+
+        # theme
+        sound-theme-freedesktop
+        nixos-artwork.wallpapers.simple-dark-gray
         mint-artwork
         mint-themes
         mint-x-icons
         mint-y-icons
         vanilla-dmz
+      ] config.environment.cinnamon.excludePackages);
 
-        # other
-        glib # for gsettings
-        shared-mime-info # for update-mime-database
-        xdg-user-dirs
-      ];
+      xdg.mime.enable = true;
+      xdg.icons.enable = true;
 
       # Override GSettings schemas
       environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-overrides}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
@@ -183,7 +193,7 @@ in
       programs.bash.vteIntegration = mkDefault true;
       programs.zsh.vteIntegration = mkDefault true;
 
-      # Harmonize Qt5 applications under Pantheon
+      # Harmonize Qt5 applications under Cinnamon
       qt5.enable = true;
       qt5.platformTheme = "gnome";
       qt5.style = "adwaita";
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/mobile.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/mobile.nix
new file mode 100644
index 0000000000000..31cc9b3deaa17
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/mobile.nix
@@ -0,0 +1,26 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  dmcfg = config.services.xserver.displayManager;
+  ldmcfg = dmcfg.lightdm;
+  cfg = ldmcfg.greeters.mobile;
+in
+{
+  options = {
+    services.xserver.displayManager.lightdm.greeters.mobile = {
+      enable = mkEnableOption (lib.mdDoc
+        "lightdm-mobile-greeter as the lightdm greeter"
+      );
+    };
+  };
+
+  config = mkIf (ldmcfg.enable && cfg.enable) {
+    services.xserver.displayManager.lightdm.greeters.gtk.enable = false;
+
+    services.xserver.displayManager.lightdm.greeter = mkDefault {
+      package = pkgs.lightdm-mobile-greeter.xgreeters;
+      name = "lightdm-mobile-greeter";
+    };
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
index b0508c3b4f793..f74e8efb8f640 100644
--- a/nixos/modules/services/x11/display-managers/lightdm.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -83,6 +83,7 @@ in
     ./lightdm-greeters/pantheon.nix
     ./lightdm-greeters/tiny.nix
     ./lightdm-greeters/slick.nix
+    ./lightdm-greeters/mobile.nix
     (mkRenamedOptionModule [ "services" "xserver" "displayManager" "lightdm" "autoLogin" "enable" ] [
       "services"
       "xserver"
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
index 34239221315db..e86a18ff618e7 100644
--- a/nixos/modules/services/x11/display-managers/sddm.nix
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -269,20 +269,5 @@ in
     # To enable user switching, allow sddm to allocate TTYs/displays dynamically.
     services.xserver.tty = null;
     services.xserver.display = null;
-
-    systemd.tmpfiles.rules = [
-      # Prior to Qt 5.9.2, there is a QML cache invalidation bug which sometimes
-      # strikes new Plasma 5 releases. If the QML cache is not invalidated, SDDM
-      # will segfault without explanation. We really tore our hair out for awhile
-      # before finding the bug:
-      # https://bugreports.qt.io/browse/QTBUG-62302
-      # We work around the problem by deleting the QML cache before startup.
-      # This was supposedly fixed in Qt 5.9.2 however it has been reported with
-      # 5.10 and 5.11 as well. The initial workaround was to delete the directory
-      # in the Xsetup script but that doesn't do anything.
-      # Instead we use tmpfiles.d to ensure it gets wiped.
-      # This causes a small but perceptible delay when SDDM starts.
-      "e ${config.users.users.sddm.home}/.cache - - - 0"
-    ];
   };
 }
diff --git a/nixos/modules/services/x11/gdk-pixbuf.nix b/nixos/modules/services/x11/gdk-pixbuf.nix
index c80e2b22792aa..2105224f92ff3 100644
--- a/nixos/modules/services/x11/gdk-pixbuf.nix
+++ b/nixos/modules/services/x11/gdk-pixbuf.nix
@@ -1,34 +1,17 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   cfg = config.services.xserver.gdk-pixbuf;
 
-  # Get packages to generate the cache for. We always include gdk-pixbuf.
-  effectivePackages = unique ([pkgs.gdk-pixbuf] ++ cfg.modulePackages);
-
-  # Generate the cache file by running gdk-pixbuf-query-loaders for each
-  # package and concatenating the results.
-  loadersCache = pkgs.runCommand "gdk-pixbuf-loaders.cache" { preferLocalBuild = true; } ''
-    (
-      for package in ${concatStringsSep " " effectivePackages}; do
-        module_dir="$package/${pkgs.gdk-pixbuf.moduleDir}"
-        if [[ ! -d $module_dir ]]; then
-          echo "Warning (services.xserver.gdk-pixbuf): missing module directory $module_dir" 1>&2
-          continue
-        fi
-        GDK_PIXBUF_MODULEDIR="$module_dir" \
-          ${pkgs.stdenv.hostPlatform.emulator pkgs.buildPackages} ${pkgs.gdk-pixbuf.dev}/bin/gdk-pixbuf-query-loaders
-      done
-    ) > "$out"
-  '';
+  loadersCache = pkgs.gnome._gdkPixbufCacheBuilder_DO_NOT_USE {
+    extraLoaders = lib.unique (cfg.modulePackages);
+  };
 in
 
 {
   options = {
-    services.xserver.gdk-pixbuf.modulePackages = mkOption {
-      type = types.listOf types.package;
+    services.xserver.gdk-pixbuf.modulePackages = lib.mkOption {
+      type = lib.types.listOf lib.types.package;
       default = [ ];
       description = lib.mdDoc "Packages providing GDK-Pixbuf modules, for cache generation.";
     };
@@ -37,7 +20,7 @@ in
   # If there is any package configured in modulePackages, we generate the
   # loaders.cache based on that and set the environment variable
   # GDK_PIXBUF_MODULE_FILE to point to it.
-  config = mkIf (cfg.modulePackages != []) {
+  config = lib.mkIf (cfg.modulePackages != []) {
     environment.variables = {
       GDK_PIXBUF_MODULE_FILE = "${loadersCache}";
     };
diff --git a/nixos/modules/services/x11/picom.nix b/nixos/modules/services/x11/picom.nix
index d42cf1d7412fe..56b55709e47fc 100644
--- a/nixos/modules/services/x11/picom.nix
+++ b/nixos/modules/services/x11/picom.nix
@@ -47,6 +47,9 @@ in {
       since picom v6 and was subsequently removed by upstream.
       See https://github.com/yshui/picom/commit/bcbc410
     '')
+    (mkRemovedOptionModule [ "services" "picom" "experimentalBackends" ] ''
+      This option was removed by upstream since picom v10.
+    '')
   ];
 
   options.services.picom = {
@@ -58,14 +61,6 @@ in {
       '';
     };
 
-    experimentalBackends = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Whether to use the unstable new reimplementation of the backends.
-      '';
-    };
-
     fade = mkOption {
       type = types.bool;
       default = false;
@@ -306,8 +301,7 @@ in {
       };
 
       serviceConfig = {
-        ExecStart = "${pkgs.picom}/bin/picom --config ${configFile}"
-          + (optionalString cfg.experimentalBackends " --experimental-backends");
+        ExecStart = "${pkgs.picom}/bin/picom --config ${configFile}";
         RestartSec = 3;
         Restart = "always";
       };
diff --git a/nixos/modules/services/x11/window-managers/default.nix b/nixos/modules/services/x11/window-managers/default.nix
index 36d5b3c8156d8..48b413beaa865 100644
--- a/nixos/modules/services/x11/window-managers/default.nix
+++ b/nixos/modules/services/x11/window-managers/default.nix
@@ -23,6 +23,7 @@ in
     ./fvwm3.nix
     ./hackedbox.nix
     ./herbstluftwm.nix
+    ./hypr.nix
     ./i3.nix
     ./jwm.nix
     ./leftwm.nix
diff --git a/nixos/modules/services/x11/window-managers/hypr.nix b/nixos/modules/services/x11/window-managers/hypr.nix
new file mode 100644
index 0000000000000..4c1fea71f93e4
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/hypr.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.hypr;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.hypr.enable = mkEnableOption (lib.mdDoc "hypr");
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "hypr";
+      start = ''
+        ${pkgs.hypr}/bin/Hypr &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.hypr ];
+  };
+}
diff --git a/nixos/modules/system/activation/specialisation.nix b/nixos/modules/system/activation/specialisation.nix
new file mode 100644
index 0000000000000..86603c8476419
--- /dev/null
+++ b/nixos/modules/system/activation/specialisation.nix
@@ -0,0 +1,85 @@
+{ config, lib, pkgs, extendModules, noUserModules, ... }:
+
+let
+  inherit (lib)
+    concatStringsSep
+    mapAttrs
+    mapAttrsToList
+    mkOption
+    types
+    ;
+
+  # This attribute is responsible for creating boot entries for
+  # child configuration. They are only (directly) accessible
+  # when the parent configuration is boot default. For example,
+  # you can provide an easy way to boot the same configuration
+  # as you use, but with another kernel
+  # !!! fix this
+  children =
+    mapAttrs
+      (childName: childConfig: childConfig.configuration.system.build.toplevel)
+      config.specialisation;
+
+in
+{
+  options = {
+
+    specialisation = mkOption {
+      default = { };
+      example = lib.literalExpression "{ fewJobsManyCores.configuration = { nix.settings = { core = 0; max-jobs = 1; }; }; }";
+      description = lib.mdDoc ''
+        Additional configurations to build. If
+        `inheritParentConfig` is true, the system
+        will be based on the overall system configuration.
+
+        To switch to a specialised configuration
+        (e.g. `fewJobsManyCores`) at runtime, run:
+
+        ```
+        sudo /run/current-system/specialisation/fewJobsManyCores/bin/switch-to-configuration test
+        ```
+      '';
+      type = types.attrsOf (types.submodule (
+        local@{ ... }:
+        let
+          extend =
+            if local.config.inheritParentConfig
+            then extendModules
+            else noUserModules.extendModules;
+        in
+        {
+          options.inheritParentConfig = mkOption {
+            type = types.bool;
+            default = true;
+            description = lib.mdDoc "Include the entire system's configuration. Set to false to make a completely differently configured system.";
+          };
+
+          options.configuration = mkOption {
+            default = { };
+            description = lib.mdDoc ''
+              Arbitrary NixOS configuration.
+
+              Anything you can add to a normal NixOS configuration, you can add
+              here, including imports and config values, although nested
+              specialisations will be ignored.
+            '';
+            visible = "shallow";
+            inherit (extend { modules = [ ./no-clone.nix ]; }) type;
+          };
+        }
+      ));
+    };
+
+  };
+
+  config = {
+    system.systemBuilderCommands = ''
+      mkdir $out/specialisation
+      ${concatStringsSep "\n"
+      (mapAttrsToList (name: path: "ln -s ${path} $out/specialisation/${name}") children)}
+    '';
+  };
+
+  # uses extendModules to generate a type
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index b71ddf95dc500..55ff98db53829 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -1,21 +1,8 @@
-{ config, lib, pkgs, extendModules, noUserModules, ... }:
+{ config, lib, pkgs, ... }:
 
 with lib;
 
 let
-
-
-  # This attribute is responsible for creating boot entries for
-  # child configuration. They are only (directly) accessible
-  # when the parent configuration is boot default. For example,
-  # you can provide an easy way to boot the same configuration
-  # as you use, but with another kernel
-  # !!! fix this
-  children =
-    mapAttrs
-      (childName: childConfig: childConfig.configuration.system.build.toplevel)
-      config.specialisation;
-
   systemBuilder =
     let
       kernelPath = "${config.boot.kernelPackages.kernel}/" +
@@ -27,7 +14,7 @@ let
 
       # Containers don't have their own kernel or initrd.  They boot
       # directly into stage 2.
-      ${optionalString (!config.boot.isContainer) ''
+      ${optionalString config.boot.kernel.enable ''
         if [ ! -f ${kernelPath} ]; then
           echo "The bootloader cannot find the proper kernel image."
           echo "(Expecting ${kernelPath})"
@@ -72,15 +59,10 @@ let
       ln -s ${config.system.path} $out/sw
       ln -s "$systemd" $out/systemd
 
-      echo -n "$configurationName" > $out/configuration-name
       echo -n "systemd ${toString config.systemd.package.interfaceVersion}" > $out/init-interface-version
       echo -n "$nixosLabel" > $out/nixos-version
       echo -n "${config.boot.kernelPackages.stdenv.hostPlatform.system}" > $out/system
 
-      mkdir $out/specialisation
-      ${concatStringsSep "\n"
-      (mapAttrsToList (name: path: "ln -s ${path} $out/specialisation/${name}") children)}
-
       mkdir $out/bin
       export localeArchive="${config.i18n.glibcLocales}/lib/locale/locale-archive"
       substituteAll ${./switch-to-configuration.pl} $out/bin/switch-to-configuration
@@ -93,6 +75,8 @@ let
         fi
       ''}
 
+      ${config.system.systemBuilderCommands}
+
       echo -n "${toString config.system.extraDependencies}" > $out/extra-dependencies
 
       ${config.system.extraSystemBuilderCmds}
@@ -103,7 +87,7 @@ let
   # kernel, systemd units, init scripts, etc.) as well as a script
   # `switch-to-configuration' that activates the configuration and
   # makes it bootable.
-  baseSystem = pkgs.stdenvNoCC.mkDerivation {
+  baseSystem = pkgs.stdenvNoCC.mkDerivation ({
     name = "nixos-system-${config.system.name}-${config.system.nixos.label}";
     preferLocalBuild = true;
     allowSubstitutes = false;
@@ -121,11 +105,9 @@ let
     dryActivationScript = config.system.dryActivationScript;
     nixosLabel = config.system.nixos.label;
 
-    configurationName = config.boot.loader.grub.configurationName;
-
     # Needed by switch-to-configuration.
     perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
-  };
+  } // config.system.systemBuilderArgs);
 
   # Handle assertions and warnings
 
@@ -140,16 +122,6 @@ let
       pkgs.replaceDependency { inherit oldDependency newDependency drv; }
     ) baseSystemAssertWarn config.system.replaceRuntimeDependencies;
 
-  /* Workaround until https://github.com/NixOS/nixpkgs/pull/156533
-     Call can be replaced by argument when that's merged.
-  */
-  tmpFixupSubmoduleBoundary = subopts:
-    lib.mkOption {
-      type = lib.types.submoduleWith {
-        modules = [ { options = subopts; } ];
-      };
-    };
-
 in
 
 {
@@ -161,49 +133,6 @@ in
 
   options = {
 
-    specialisation = mkOption {
-      default = {};
-      example = lib.literalExpression "{ fewJobsManyCores.configuration = { nix.settings = { core = 0; max-jobs = 1; }; }; }";
-      description = lib.mdDoc ''
-        Additional configurations to build. If
-        `inheritParentConfig` is true, the system
-        will be based on the overall system configuration.
-
-        To switch to a specialised configuration
-        (e.g. `fewJobsManyCores`) at runtime, run:
-
-        ```
-        sudo /run/current-system/specialisation/fewJobsManyCores/bin/switch-to-configuration test
-        ```
-      '';
-      type = types.attrsOf (types.submodule (
-        local@{ ... }: let
-          extend = if local.config.inheritParentConfig
-            then extendModules
-            else noUserModules.extendModules;
-        in {
-          options.inheritParentConfig = mkOption {
-            type = types.bool;
-            default = true;
-            description = lib.mdDoc "Include the entire system's configuration. Set to false to make a completely differently configured system.";
-          };
-
-          options.configuration = mkOption {
-            default = {};
-            description = lib.mdDoc ''
-              Arbitrary NixOS configuration.
-
-              Anything you can add to a normal NixOS configuration, you can add
-              here, including imports and config values, although nested
-              specialisations will be ignored.
-            '';
-            visible = "shallow";
-            inherit (extend { modules = [ ./no-clone.nix ]; }) type;
-          };
-        })
-      );
-    };
-
     system.boot.loader.id = mkOption {
       internal = true;
       default = "";
@@ -231,7 +160,7 @@ in
       '';
     };
 
-    system.build = tmpFixupSubmoduleBoundary {
+    system.build = {
       installBootLoader = mkOption {
         internal = true;
         # "; true" => make the `$out` argument from switch-to-configuration.pl
@@ -276,6 +205,24 @@ in
       '';
     };
 
+    system.systemBuilderCommands = mkOption {
+      type = types.lines;
+      internal = true;
+      default = "";
+      description = ''
+        This code will be added to the builder creating the system store path.
+      '';
+    };
+
+    system.systemBuilderArgs = mkOption {
+      type = types.attrsOf types.unspecified;
+      internal = true;
+      default = {};
+      description = lib.mdDoc ''
+        `lib.mkDerivation` attributes that will be passed to the top level system builder.
+      '';
+    };
+
     system.extraSystemBuilderCmds = mkOption {
       type = types.lines;
       internal = true;
@@ -357,6 +304,4 @@ in
 
   };
 
-  # uses extendModules to generate a type
-  meta.buildDocsInSandbox = false;
 }
diff --git a/nixos/modules/system/boot/binfmt.nix b/nixos/modules/system/boot/binfmt.nix
index 260638ceed50f..87e66f73be0ec 100644
--- a/nixos/modules/system/boot/binfmt.nix
+++ b/nixos/modules/system/boot/binfmt.nix
@@ -321,5 +321,6 @@ in {
       "proc-sys-fs-binfmt_misc.mount"
       "systemd-binfmt.service"
     ];
+    systemd.services.systemd-binfmt.restartTriggers = [ (builtins.toJSON config.boot.binfmt.registrations) ];
   };
 }
diff --git a/nixos/modules/system/boot/initrd-ssh.nix b/nixos/modules/system/boot/initrd-ssh.nix
index 256aa608ac434..673655f20ee84 100644
--- a/nixos/modules/system/boot/initrd-ssh.nix
+++ b/nixos/modules/system/boot/initrd-ssh.nix
@@ -25,7 +25,7 @@ in
     };
 
     port = mkOption {
-      type = types.int;
+      type = types.port;
       default = 22;
       description = lib.mdDoc ''
         Port on which SSH initrd service should listen.
diff --git a/nixos/modules/system/boot/kernel.nix b/nixos/modules/system/boot/kernel.nix
index 8a1630f7e3ebb..6783f8ec62ff9 100644
--- a/nixos/modules/system/boot/kernel.nix
+++ b/nixos/modules/system/boot/kernel.nix
@@ -20,6 +20,9 @@ in
   ###### interface
 
   options = {
+    boot.kernel.enable = mkEnableOption (lib.mdDoc "the Linux kernel. This is useful for systemd-like containers which do not require a kernel.") // {
+      default = true;
+    };
 
     boot.kernel.features = mkOption {
       default = {};
@@ -258,7 +261,7 @@ in
           ];
       })
 
-      (mkIf (!config.boot.isContainer) {
+      (mkIf config.boot.kernel.enable {
         system.build = { inherit kernel; };
 
         system.modulesTree = [ kernel ] ++ config.boot.extraModulePackages;
diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix
index 1b075133ff40c..a67b10608aa74 100644
--- a/nixos/modules/system/boot/loader/grub/grub.nix
+++ b/nixos/modules/system/boot/loader/grub/grub.nix
@@ -748,6 +748,11 @@ in
 
       boot.loader.supportsInitrdSecrets = true;
 
+      system.systemBuilderArgs.configurationName = cfg.configurationName;
+      system.systemBuilderCommands = ''
+        echo -n "$configurationName" > $out/configuration-name
+      '';
+
       system.build.installBootLoader =
         let
           install-grub-pl = pkgs.substituteAll {
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 a9d43d027e011..8cb7c7b8e47bb 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
@@ -57,6 +57,12 @@ let
       --disallow-untyped-defs \
       $out
   '';
+
+  finalSystemdBootBuilder = pkgs.writeScript "install-systemd-boot.sh" ''
+    #!${pkgs.runtimeShell}
+    ${checkedSystemdBootBuilder} "$@"
+    ${cfg.extraInstallCommands}
+  '';
 in {
 
   imports =
@@ -99,6 +105,22 @@ in {
       '';
     };
 
+    extraInstallCommands = mkOption {
+      default = "";
+      example = ''
+        default_cfg=$(cat /boot/loader/loader.conf | grep default | awk '{print $2}')
+        init_value=$(cat /boot/loader/entries/$default_cfg | grep init= | awk '{print $2}')
+        sed -i "s|@INIT@|$init_value|g" /boot/custom/config_with_placeholder.conf
+      '';
+      type = types.lines;
+      description = lib.mdDoc ''
+        Additional shell commands inserted in the bootloader installer
+        script after generating menu entries. It can be used to expand
+        on extra boot entries that cannot incorporate certain pieces of
+        information (such as the resulting `init=` kernel parameter).
+      '';
+    };
+
     consoleMode = mkOption {
       default = "keep";
 
@@ -277,7 +299,7 @@ in {
     ];
 
     system = {
-      build.installBootLoader = checkedSystemdBootBuilder;
+      build.installBootLoader = finalSystemdBootBuilder;
 
       boot.loader.id = "systemd-boot";
 
diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix
index 02b020b61eb60..03d03cb348e82 100644
--- a/nixos/modules/system/boot/luksroot.nix
+++ b/nixos/modules/system/boot/luksroot.nix
@@ -905,9 +905,11 @@ in
         { assertion = config.boot.initrd.systemd.enable -> !luks.gpgSupport;
           message = "systemd stage 1 does not support GPG smartcards yet.";
         }
-        # TODO
         { assertion = config.boot.initrd.systemd.enable -> !luks.fido2Support;
-          message = "systemd stage 1 does not support FIDO2 yet.";
+          message = ''
+            systemd stage 1 does not support configuring FIDO2 unlocking through `boot.initrd.luks.devices.<name>.fido2`.
+            Use systemd-cryptenroll(1) to configure FIDO2 support.
+          '';
         }
         # TODO
         { assertion = config.boot.initrd.systemd.enable -> !luks.yubikeySupport;
diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
index a9b81dd116bbd..28abf820ec097 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -584,6 +584,7 @@ let
           "Label"
           "PreferredLifetime"
           "Scope"
+          "RouteMetric"
           "HomeAddress"
           "DuplicateAddressDetection"
           "ManageTemporaryAddress"
@@ -592,6 +593,7 @@ let
         ])
         (assertHasField "Address")
         (assertValueOneOf "PreferredLifetime" ["forever" "infinity" "0" 0])
+        (assertInt "RouteMetric")
         (assertValueOneOf "HomeAddress" boolValues)
         (assertValueOneOf "DuplicateAddressDetection" ["ipv4" "ipv6" "both" "none"])
         (assertValueOneOf "ManageTemporaryAddress" boolValues)
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index 8f2044a0985eb..8f1086c9c539e 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -151,6 +151,9 @@ let
     ] ++ optionals cfg.package.withHostnamed [
       "dbus-org.freedesktop.hostname1.service"
       "systemd-hostnamed.service"
+    ] ++ optionals cfg.package.withPortabled [
+      "dbus-org.freedesktop.portable1.service"
+      "systemd-portabled.service"
     ] ++ [
       "systemd-exit.service"
       "systemd-update-done.service"
@@ -555,7 +558,8 @@ in
       # Environment of PID 1
       systemd.managerEnvironment = {
         # Doesn't contain systemd itself - everything works so it seems to use the compiled-in value for its tools
-        PATH = lib.makeBinPath config.system.fsPackages;
+        # util-linux is needed for the main fsck utility wrapping the fs-specific ones
+        PATH = lib.makeBinPath (config.system.fsPackages ++ [cfg.package.util-linux]);
         LOCALE_ARCHIVE = "/run/current-system/sw/lib/locale/locale-archive";
         TZDIR = "/etc/zoneinfo";
         # If SYSTEMD_UNIT_PATH ends with an empty component (":"), the usual unit load path will be appended to the contents of the variable
diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix
index 03f94c426cb09..31702499b0f14 100644
--- a/nixos/modules/system/boot/systemd/initrd.nix
+++ b/nixos/modules/system/boot/systemd/initrd.nix
@@ -332,7 +332,10 @@ in {
   config = mkIf (config.boot.initrd.enable && cfg.enable) {
     system.build = { inherit initialRamdisk; };
 
-    boot.initrd.availableKernelModules = [ "autofs4" ]; # systemd needs this for some features
+    boot.initrd.availableKernelModules = [
+      "autofs4"           # systemd needs this for some features
+      "tpm-tis" "tpm-crb" # systemd-cryptenroll
+    ];
 
     boot.initrd.systemd = {
       initrdBin = [pkgs.bash pkgs.coreutils cfg.package.kmod cfg.package] ++ config.system.fsPackages;
@@ -403,6 +406,17 @@ in {
 
         # so NSS can look up usernames
         "${pkgs.glibc}/lib/libnss_files.so.2"
+      ] ++ optionals cfg.package.withCryptsetup [
+        # tpm2 support
+        "${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-tpm2.so"
+        pkgs.tpm2-tss
+
+        # fido2 support
+        "${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-fido2.so"
+        "${pkgs.libfido2}/lib/libfido2.so.1"
+
+        # the unwrapped systemd-cryptsetup executable
+        "${cfg.package}/lib/systemd/.systemd-cryptsetup-wrapped"
       ] ++ jobScripts;
 
       targets.initrd.aliases = ["default.target"];
diff --git a/nixos/modules/system/boot/systemd/logind.nix b/nixos/modules/system/boot/systemd/logind.nix
index 5980160321367..b0c927f19f9d7 100644
--- a/nixos/modules/system/boot/systemd/logind.nix
+++ b/nixos/modules/system/boot/systemd/logind.nix
@@ -82,6 +82,8 @@ in
       "dbus-org.freedesktop.import1.service"
     ] ++ optionals config.systemd.package.withMachined [
       "dbus-org.freedesktop.machine1.service"
+    ] ++ optionals config.systemd.package.withPortabled [
+      "dbus-org.freedesktop.portable1.service"
     ] ++ [
       "dbus-org.freedesktop.login1.service"
       "user@.service"
diff --git a/nixos/modules/system/boot/systemd/nspawn.nix b/nixos/modules/system/boot/systemd/nspawn.nix
index d9e42ad5b26b1..cbc89554c9fd9 100644
--- a/nixos/modules/system/boot/systemd/nspawn.nix
+++ b/nixos/modules/system/boot/systemd/nspawn.nix
@@ -45,7 +45,9 @@ let
   ];
 
   instanceOptions = {
-    options = sharedOptions // {
+    options =
+    (getAttrs [ "enable" ] sharedOptions)
+    // {
       execConfig = mkOption {
         default = {};
         example = { Parameters = "/bin/sh"; };
diff --git a/nixos/modules/system/boot/systemd/tmpfiles.nix b/nixos/modules/system/boot/systemd/tmpfiles.nix
index e990e953b0572..32b9b275d3587 100644
--- a/nixos/modules/system/boot/systemd/tmpfiles.nix
+++ b/nixos/modules/system/boot/systemd/tmpfiles.nix
@@ -79,6 +79,7 @@ in
 
         ln -s "${systemd}/example/tmpfiles.d/home.conf"
         ln -s "${systemd}/example/tmpfiles.d/journal-nocow.conf"
+        ln -s "${systemd}/example/tmpfiles.d/portables.conf"
         ln -s "${systemd}/example/tmpfiles.d/static-nodes-permissions.conf"
         ln -s "${systemd}/example/tmpfiles.d/systemd.conf"
         ln -s "${systemd}/example/tmpfiles.d/systemd-nologin.conf"
diff --git a/nixos/modules/system/boot/uvesafb.nix b/nixos/modules/system/boot/uvesafb.nix
new file mode 100644
index 0000000000000..b10dc42887a15
--- /dev/null
+++ b/nixos/modules/system/boot/uvesafb.nix
@@ -0,0 +1,39 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.boot.uvesafb;
+  inherit (lib) mkIf mkEnableOption mkOption mdDoc types;
+in {
+  options = {
+    boot.uvesafb = {
+      enable = mkEnableOption (mdDoc "uvesafb");
+
+      gfx-mode = mkOption {
+        type = types.str;
+        default = "1024x768-32";
+        description = mdDoc "Screen resolution in modedb format. See [uvesafb](https://docs.kernel.org/fb/uvesafb.html) and [modedb](https://docs.kernel.org/fb/modedb.html) documentation for more details. The default value is a sensible default but may be not ideal for all setups.";
+      };
+
+      v86d.package = mkOption {
+        type = types.package;
+        description = mdDoc "Which v86d package to use with uvesafb";
+        defaultText = ''config.boot.kernelPackages.v86d.overrideAttrs (old: {
+          hardeningDisable = [ "all" ];
+        })'';
+        default = config.boot.kernelPackages.v86d.overrideAttrs (old: {
+          hardeningDisable = [ "all" ];
+        });
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    boot.initrd = {
+      kernelModules = [ "uvesafb" ];
+      extraFiles."/usr/v86d".source = cfg.v86d.package;
+    };
+
+    boot.kernelParams = [
+      "video=uvesafb:mode:${cfg.gfx-mode},mtrr:3,ywrap"
+      ''uvesafb.v86d="${cfg.v86d.package}/bin/v86d"''
+    ];
+  };
+}
diff --git a/nixos/modules/system/etc/setup-etc.pl b/nixos/modules/system/etc/setup-etc.pl
index be6b2d9ae71ef..a048261a3df11 100644
--- a/nixos/modules/system/etc/setup-etc.pl
+++ b/nixos/modules/system/etc/setup-etc.pl
@@ -137,7 +137,7 @@ foreach my $fn (@oldCopied) {
 
 # Rewrite /etc/.clean.
 close CLEAN;
-write_file("/etc/.clean", map { "$_\n" } @copied);
+write_file("/etc/.clean", map { "$_\n" } sort @copied);
 
 # Create /etc/NIXOS tag if not exists.
 # When /etc is not on a persistent filesystem, it will be wiped after reboot,
diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix
index 994747601309c..399ea9eabe08d 100644
--- a/nixos/modules/tasks/filesystems.nix
+++ b/nixos/modules/tasks/filesystems.nix
@@ -155,7 +155,7 @@ let
 
   makeFstabEntries =
     let
-      fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "vboxsf" "glusterfs" "apfs" "9p" "cifs" "prl_fs" "vmhgfs" ];
+      fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "nfs4" "vboxsf" "glusterfs" "apfs" "9p" "cifs" "prl_fs" "vmhgfs" ];
       isBindMount = fs: builtins.elem "bind" fs.options;
       skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck || isBindMount fs;
       # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
index 96222f3b4f645..4b4f4cc801aba 100644
--- a/nixos/modules/tasks/filesystems/zfs.nix
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -226,6 +226,15 @@ in
           '';
       };
 
+      allowHibernation = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Allow hibernation support, this may be a unsafe option depending on your
+          setup. Make sure to NOT use Swap on ZFS.
+        '';
+      };
+
       extraPools = mkOption {
         type = types.listOf types.str;
         default = [];
@@ -498,6 +507,10 @@ in
 
       boot = {
         kernelModules = [ "zfs" ];
+        # https://github.com/openzfs/zfs/issues/260
+        # https://github.com/openzfs/zfs/issues/12842
+        # https://github.com/NixOS/nixpkgs/issues/106093
+        kernelParams = lib.optionals (!config.boot.zfs.allowHibernation) [ "nohibernate" ];
 
         extraModulePackages = [
           (if config.boot.zfs.enableUnstable then
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index 6f01917bcc59f..b7a4282f9727f 100644
--- a/nixos/modules/tasks/network-interfaces.nix
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -473,9 +473,29 @@ in
       defaultText = literalExpression ''"''${networking.hostName}.''${networking.domain}"'';
       description = lib.mdDoc ''
         The fully qualified domain name (FQDN) of this host. It is the result
-        of combining networking.hostName and networking.domain. Using this
+        of combining `networking.hostName` and `networking.domain.` Using this
         option will result in an evaluation error if the hostname is empty or
         no domain is specified.
+
+        Modules that accept a mere `networing.hostName` but prefer a fully qualified
+        domain name may use `networking.fqdnOrHostName` instead.
+      '';
+    };
+
+    networking.fqdnOrHostName = mkOption {
+      readOnly = true;
+      type = types.str;
+      default = if cfg.domain == null then cfg.hostName else cfg.fqdn;
+      defaultText = literalExpression ''
+        if cfg.domain == null then cfg.hostName else cfg.fqdn
+      '';
+      description = lib.mdDoc ''
+        Either the fully qualified domain name (FQDN), or just the host name if
+        it does not exists.
+
+        This is a convenience option for modules to read instead of `fqdn` when
+        a mere `hostName` is also an acceptable value; this option does not
+        throw an error when `domain` is unset.
       '';
     };
 
@@ -1356,8 +1376,8 @@ in
       "net.ipv6.conf.default.disable_ipv6" = mkDefault (!cfg.enableIPv6);
       # networkmanager falls back to "/proc/sys/net/ipv6/conf/default/use_tempaddr"
       "net.ipv6.conf.default.use_tempaddr" = tempaddrValues.${cfg.tempAddresses}.sysctl;
-    } // listToAttrs (flip concatMap (filter (i: i.proxyARP) interfaces)
-        (i: [(nameValuePair "net.ipv4.conf.${replaceChars ["."] ["/"] i.name}.proxy_arp" true)]))
+    } // listToAttrs (forEach interfaces
+        (i: nameValuePair "net.ipv4.conf.${replaceChars ["."] ["/"] i.name}.proxy_arp" i.proxyARP))
       // listToAttrs (forEach interfaces
         (i: let
           opt = i.tempAddress;
diff --git a/nixos/modules/virtualisation/container-config.nix b/nixos/modules/virtualisation/container-config.nix
index 94f28ea80d094..09a2d9de040a2 100644
--- a/nixos/modules/virtualisation/container-config.nix
+++ b/nixos/modules/virtualisation/container-config.nix
@@ -7,6 +7,11 @@ with lib;
   config = mkIf config.boot.isContainer {
 
     # Disable some features that are not useful in a container.
+
+    boot.kernel.enable = false;
+
+    console.enable = mkDefault false;
+
     nix.optimise.automatic = mkDefault false; # the store is host managed
     powerManagement.enable = mkDefault false;
     documentation.nixos.enable = mkDefault false;
@@ -16,6 +21,9 @@ with lib;
     # Containers should be light-weight, so start sshd on demand.
     services.openssh.startWhenNeeded = mkDefault true;
 
+    # containers do not need to setup devices
+    services.udev.enable = false;
+
     # Shut up warnings about not having a boot loader.
     system.build.installBootLoader = lib.mkDefault "${pkgs.coreutils}/bin/true";
 
diff --git a/nixos/modules/virtualisation/libvirtd.nix b/nixos/modules/virtualisation/libvirtd.nix
index be6ebb3eacc99..3b30bc8c41658 100644
--- a/nixos/modules/virtualisation/libvirtd.nix
+++ b/nixos/modules/virtualisation/libvirtd.nix
@@ -336,6 +336,7 @@ in
     };
 
     systemd.services.libvirtd = {
+      wantedBy = [ "multi-user.target" ];
       requires = [ "libvirtd-config.service" ];
       after = [ "libvirtd-config.service" ]
         ++ optional vswitch.enable "ovs-vswitchd.service";
diff --git a/nixos/modules/virtualisation/nixos-containers.nix b/nixos/modules/virtualisation/nixos-containers.nix
index 22be1d5bff92e..e1e640c447425 100644
--- a/nixos/modules/virtualisation/nixos-containers.nix
+++ b/nixos/modules/virtualisation/nixos-containers.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, ... }@host:
 
 with lib;
 
@@ -9,6 +9,10 @@ let
   configurationDirectory = "/etc/${configurationDirectoryName}";
   stateDirectory = "/var/lib/${configurationPrefix}containers";
 
+  nixos-container = pkgs.nixos-container.override {
+    inherit stateDirectory configurationDirectory;
+  };
+
   # The container's init script, a small wrapper around the regular
   # NixOS stage-2 init script.
   containerInit = (cfg:
@@ -138,6 +142,8 @@ let
         fi
       ''}
 
+      export SYSTEMD_NSPAWN_UNIFIED_HIERARCHY=1
+
       # Run systemd-nspawn without startup notification (we'll
       # wait for the container systemd to signal readiness)
       # Kill signal handling means systemd-nspawn will pass a system-halt signal
@@ -248,7 +254,7 @@ let
     ExecReload = pkgs.writeScript "reload-container"
       ''
         #! ${pkgs.runtimeShell} -e
-        ${pkgs.nixos-container}/bin/nixos-container run "$INSTANCE" -- \
+        ${nixos-container}/bin/nixos-container run "$INSTANCE" -- \
           bash --login -c "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/bin/switch-to-configuration test"
       '';
 
@@ -284,7 +290,6 @@ let
     DeviceAllow = map (d: "${d.node} ${d.modifier}") cfg.allowedDevices;
   };
 
-  inherit (config.nixpkgs) localSystem;
   kernelVersion = config.boot.kernelPackages.kernel.version;
 
   bindMountOpts = { name, ... }: {
@@ -480,10 +485,13 @@ in
                 merge = loc: defs: (import "${toString config.nixpkgs}/nixos/lib/eval-config.nix" {
                   modules =
                     let
-                      extraConfig = {
+                      extraConfig = { options, ... }: {
                         _file = "module at ${__curPos.file}:${toString __curPos.line}";
                         config = {
-                          nixpkgs = { inherit localSystem; };
+                          nixpkgs = if options.nixpkgs?hostPlatform && host.options.nixpkgs.hostPlatform.isDefined
+                                    then { inherit (host.config.nixpkgs) hostPlatform; }
+                                    else { inherit (host.config.nixpkgs) localSystem; }
+                          ;
                           boot.isContainer = true;
                           networking.hostName = mkDefault name;
                           networking.useDHCP = false;
@@ -720,7 +728,7 @@ in
               { config =
                   { config, pkgs, ... }:
                   { services.postgresql.enable = true;
-                    services.postgresql.package = pkgs.postgresql_10;
+                    services.postgresql.package = pkgs.postgresql_14;
 
                     system.stateVersion = "21.05";
                   };
@@ -864,9 +872,7 @@ in
     '';
 
     environment.systemPackages = [
-      (pkgs.nixos-container.override {
-        inherit stateDirectory configurationDirectory;
-      })
+      nixos-container
     ];
 
     boot.kernelModules = [
diff --git a/nixos/modules/virtualisation/vmware-guest.nix b/nixos/modules/virtualisation/vmware-guest.nix
index 3b4d484fc8b9f..b8f0a4cf668ef 100644
--- a/nixos/modules/virtualisation/vmware-guest.nix
+++ b/nixos/modules/virtualisation/vmware-guest.nix
@@ -16,7 +16,8 @@ in
     enable = mkEnableOption (lib.mdDoc "VMWare Guest Support");
     headless = mkOption {
       type = types.bool;
-      default = false;
+      default = !config.services.xserver.enable;
+      defaultText = "!config.services.xserver.enable";
       description = lib.mdDoc "Whether to disable X11-related features.";
     };
   };
diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix
index a11ee31ab8d04..bd7b452735f1c 100644
--- a/nixos/release-combined.nix
+++ b/nixos/release-combined.nix
@@ -55,6 +55,7 @@ in rec {
         (onFullSupported "nixos.manual")
         (onSystems ["x86_64-linux"] "nixos.ova")
         (onSystems ["aarch64-linux"] "nixos.sd_image")
+        (onFullSupported "nixos.tests.acme")
         (onSystems ["x86_64-linux"] "nixos.tests.boot.biosCdrom")
         (onSystems ["x86_64-linux"] "nixos.tests.boot.biosUsb")
         (onFullSupported "nixos.tests.boot-stage1")
diff --git a/nixos/release-small.nix b/nixos/release-small.nix
index 1719d6738c5cd..deb428d1bec05 100644
--- a/nixos/release-small.nix
+++ b/nixos/release-small.nix
@@ -31,6 +31,7 @@ in rec {
     inherit (nixos') channel manual options iso_minimal amazonImage dummy;
     tests = {
       inherit (nixos'.tests)
+        acme
         containers-imperative
         containers-ip
         firewall
@@ -110,6 +111,7 @@ in rec {
         "nixos.iso_minimal"
         "nixos.amazonImage"
         "nixos.manual"
+        "nixos.tests.acme"
         "nixos.tests.boot.uefiCdrom"
         "nixos.tests.containers-imperative"
         "nixos.tests.containers-ip"
diff --git a/nixos/tests/adguardhome.nix b/nixos/tests/adguardhome.nix
index 1a220f9969985..ec1cc1e497b50 100644
--- a/nixos/tests/adguardhome.nix
+++ b/nixos/tests/adguardhome.nix
@@ -2,8 +2,13 @@
   name = "adguardhome";
 
   nodes = {
-    minimalConf = { ... }: {
-      services.adguardhome = { enable = true; };
+    nullConf = { ... }: { services.adguardhome = { enable = true; }; };
+
+    emptyConf = { lib, ... }: {
+      services.adguardhome = {
+        enable = true;
+        settings = {};
+      };
     };
 
     declarativeConf = { ... }: {
@@ -12,6 +17,7 @@
 
         mutableSettings = false;
         settings = {
+          schema_version = 0;
           dns = {
             bind_host = "0.0.0.0";
             bootstrap_dns = "127.0.0.1";
@@ -26,6 +32,7 @@
 
         mutableSettings = true;
         settings = {
+          schema_version = 0;
           dns = {
             bind_host = "0.0.0.0";
             bootstrap_dns = "127.0.0.1";
@@ -36,9 +43,12 @@
   };
 
   testScript = ''
-    with subtest("Minimal config test"):
-        minimalConf.wait_for_unit("adguardhome.service")
-        minimalConf.wait_for_open_port(3000)
+    with subtest("Minimal (settings = null) config test"):
+        nullConf.wait_for_unit("adguardhome.service")
+
+    with subtest("Default config test"):
+        emptyConf.wait_for_unit("adguardhome.service")
+        emptyConf.wait_for_open_port(3000)
 
     with subtest("Declarative config test, DNS will be reachable"):
         declarativeConf.wait_for_unit("adguardhome.service")
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 0fc08e841ec07..bd0fe18c4f8c2 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -102,6 +102,7 @@ in {
   brscan5 = handleTest ./brscan5.nix {};
   btrbk = handleTest ./btrbk.nix {};
   btrbk-no-timer = handleTest ./btrbk-no-timer.nix {};
+  btrbk-section-order = handleTest ./btrbk-section-order.nix {};
   buildbot = handleTest ./buildbot.nix {};
   buildkite-agents = handleTest ./buildkite-agents.nix {};
   caddy = handleTest ./caddy.nix {};
@@ -109,8 +110,6 @@ in {
   cage = handleTest ./cage.nix {};
   cagebreak = handleTest ./cagebreak.nix {};
   calibre-web = handleTest ./calibre-web.nix {};
-  cassandra_2_1 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_2_1; };
-  cassandra_2_2 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_2_2; };
   cassandra_3_0 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_3_0; };
   cassandra_3_11 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_3_11; };
   ceph-multi-node = handleTestOn ["x86_64-linux"] ./ceph-multi-node.nix {};
@@ -124,6 +123,7 @@ in {
   cjdns = handleTest ./cjdns.nix {};
   clickhouse = handleTest ./clickhouse.nix {};
   cloud-init = handleTest ./cloud-init.nix {};
+  cloud-init-hostname = handleTest ./cloud-init-hostname.nix {};
   cntr = handleTestOn ["aarch64-linux" "x86_64-linux"] ./cntr.nix {};
   cockroachdb = handleTestOn ["x86_64-linux"] ./cockroachdb.nix {};
   collectd = handleTest ./collectd.nix {};
@@ -143,6 +143,7 @@ in {
   containers-reloadable = handleTest ./containers-reloadable.nix {};
   containers-restart_networking = handleTest ./containers-restart_networking.nix {};
   containers-tmpfs = handleTest ./containers-tmpfs.nix {};
+  containers-unified-hierarchy = handleTest ./containers-unified-hierarchy.nix {};
   convos = handleTest ./convos.nix {};
   corerad = handleTest ./corerad.nix {};
   coturn = handleTest ./coturn.nix {};
@@ -170,6 +171,7 @@ in {
   documentation = pkgs.callPackage ../modules/misc/documentation/test.nix { inherit nixosLib; };
   doh-proxy-rust = handleTest ./doh-proxy-rust.nix {};
   dokuwiki = handleTest ./dokuwiki.nix {};
+  dolibarr = handleTest ./dolibarr.nix {};
   domination = handleTest ./domination.nix {};
   dovecot = handleTest ./dovecot.nix {};
   drbd = handleTest ./drbd.nix {};
@@ -177,9 +179,11 @@ in {
   ec2-config = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-config or {};
   ec2-nixops = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-nixops or {};
   ecryptfs = handleTest ./ecryptfs.nix {};
+  fscrypt = handleTest ./fscrypt.nix {};
   ejabberd = handleTest ./xmpp/ejabberd.nix {};
   elk = handleTestOn ["x86_64-linux"] ./elk.nix {};
   emacs-daemon = handleTest ./emacs-daemon.nix {};
+  endlessh = handleTest ./endlessh.nix {};
   endlessh-go = handleTest ./endlessh-go.nix {};
   engelsystem = handleTest ./engelsystem.nix {};
   enlightenment = handleTest ./enlightenment.nix {};
@@ -213,6 +217,7 @@ in {
   fsck = handleTest ./fsck.nix {};
   ft2-clone = handleTest ./ft2-clone.nix {};
   mimir = handleTest ./mimir.nix {};
+  garage = handleTest ./garage.nix {};
   gerrit = handleTest ./gerrit.nix {};
   geth = handleTest ./geth.nix {};
   ghostunnel = handleTest ./ghostunnel.nix {};
@@ -231,7 +236,7 @@ in {
   gollum = handleTest ./gollum.nix {};
   google-oslogin = handleTest ./google-oslogin {};
   gotify-server = handleTest ./gotify-server.nix {};
-  grafana = handleTest ./grafana.nix {};
+  grafana = handleTest ./grafana {};
   grafana-agent = handleTest ./grafana-agent.nix {};
   graphite = handleTest ./graphite.nix {};
   graylog = handleTest ./graylog.nix {};
@@ -299,6 +304,7 @@ in {
   k3s = handleTest ./k3s {};
   kafka = handleTest ./kafka.nix {};
   kanidm = handleTest ./kanidm.nix {};
+  karma = handleTest ./karma.nix {};
   kbd-setfont-decompress = handleTest ./kbd-setfont-decompress.nix {};
   kbd-update-search-paths-patch = handleTest ./kbd-update-search-paths-patch.nix {};
   kea = handleTest ./kea.nix {};
@@ -364,6 +370,7 @@ in {
   mediawiki = handleTest ./mediawiki.nix {};
   meilisearch = handleTest ./meilisearch.nix {};
   memcached = handleTest ./memcached.nix {};
+  merecat = handleTest ./merecat.nix {};
   metabase = handleTest ./metabase.nix {};
   minecraft = handleTest ./minecraft.nix {};
   minecraft-server = handleTest ./minecraft-server.nix {};
@@ -424,6 +431,7 @@ in {
   nginx-etag = handleTest ./nginx-etag.nix {};
   nginx-http3 = handleTest ./nginx-http3.nix {};
   nginx-modsecurity = handleTest ./nginx-modsecurity.nix {};
+  nginx-njs = handleTest ./nginx-njs.nix {};
   nginx-pubhtml = handleTest ./nginx-pubhtml.nix {};
   nginx-sandbox = handleTestOn ["x86_64-linux"] ./nginx-sandbox.nix {};
   nginx-sso = handleTest ./nginx-sso.nix {};
@@ -443,6 +451,7 @@ in {
   novacomd = handleTestOn ["x86_64-linux"] ./novacomd.nix {};
   nscd = handleTest ./nscd.nix {};
   nsd = handleTest ./nsd.nix {};
+  ntfy-sh = handleTest ./ntfy-sh.nix {};
   nzbget = handleTest ./nzbget.nix {};
   nzbhydra2 = handleTest ./nzbhydra2.nix {};
   oh-my-zsh = handleTest ./oh-my-zsh.nix {};
@@ -527,6 +536,7 @@ in {
   pulseaudio = discoverTests (import ./pulseaudio.nix);
   qboot = handleTestOn ["x86_64-linux" "i686-linux"] ./qboot.nix {};
   quorum = handleTest ./quorum.nix {};
+  quake3 = handleTest ./quake3.nix {};
   rabbitmq = handleTest ./rabbitmq.nix {};
   radarr = handleTest ./radarr.nix {};
   radicale = handleTest ./radicale.nix {};
@@ -598,8 +608,10 @@ in {
   systemd-cryptenroll = handleTest ./systemd-cryptenroll.nix {};
   systemd-escaping = handleTest ./systemd-escaping.nix {};
   systemd-initrd-btrfs-raid = handleTest ./systemd-initrd-btrfs-raid.nix {};
+  systemd-initrd-luks-fido2 = handleTest ./systemd-initrd-luks-fido2.nix {};
   systemd-initrd-luks-keyfile = handleTest ./systemd-initrd-luks-keyfile.nix {};
   systemd-initrd-luks-password = handleTest ./systemd-initrd-luks-password.nix {};
+  systemd-initrd-luks-tpm2 = handleTest ./systemd-initrd-luks-tpm2.nix {};
   systemd-initrd-modprobe = handleTest ./systemd-initrd-modprobe.nix {};
   systemd-initrd-shutdown = handleTest ./systemd-shutdown.nix { systemdStage1 = true; };
   systemd-initrd-simple = handleTest ./systemd-initrd-simple.nix {};
@@ -611,8 +623,10 @@ in {
   systemd-networkd-dhcpserver-static-leases = handleTest ./systemd-networkd-dhcpserver-static-leases.nix {};
   systemd-networkd-ipv6-prefix-delegation = handleTest ./systemd-networkd-ipv6-prefix-delegation.nix {};
   systemd-networkd-vrf = handleTest ./systemd-networkd-vrf.nix {};
+  systemd-no-tainted = handleTest ./systemd-no-tainted.nix {};
   systemd-nspawn = handleTest ./systemd-nspawn.nix {};
   systemd-oomd = handleTest ./systemd-oomd.nix {};
+  systemd-portabled = handleTest ./systemd-portabled.nix {};
   systemd-shutdown = handleTest ./systemd-shutdown.nix {};
   systemd-timesyncd = handleTest ./systemd-timesyncd.nix {};
   systemd-misc = handleTest ./systemd-misc.nix {};
@@ -652,13 +666,13 @@ in {
   unit-php = handleTest ./web-servers/unit-php.nix {};
   upnp = handleTest ./upnp.nix {};
   uptermd = handleTest ./uptermd.nix {};
+  uptime-kuma = handleTest ./uptime-kuma.nix {};
   usbguard = handleTest ./usbguard.nix {};
   user-activation-scripts = handleTest ./user-activation-scripts.nix {};
   user-home-mode = handleTest ./user-home-mode.nix {};
   uwsgi = handleTest ./uwsgi.nix {};
   v2ray = handleTest ./v2ray.nix {};
   varnish60 = handleTest ./varnish.nix { package = pkgs.varnish60; };
-  varnish71 = handleTest ./varnish.nix { package = pkgs.varnish71; };
   varnish72 = handleTest ./varnish.nix { package = pkgs.varnish72; };
   vault = handleTest ./vault.nix {};
   vault-dev = handleTest ./vault-dev.nix {};
@@ -679,6 +693,7 @@ in {
   wmderland = handleTest ./wmderland.nix {};
   wpa_supplicant = handleTest ./wpa_supplicant.nix {};
   wordpress = handleTest ./wordpress.nix {};
+  wrappers = handleTest ./wrappers.nix {};
   writefreely = handleTest ./web-apps/writefreely.nix {};
   xandikos = handleTest ./xandikos.nix {};
   xautolock = handleTest ./xautolock.nix {};
diff --git a/nixos/tests/bazarr.nix b/nixos/tests/bazarr.nix
index efcd9de010804..13b27bb530c04 100644
--- a/nixos/tests/bazarr.nix
+++ b/nixos/tests/bazarr.nix
@@ -16,11 +16,15 @@ in
         enable = true;
         listenPort = port;
       };
+      nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) ["unrar"];
+      # Workaround for https://github.com/morpheus65535/bazarr/issues/1983
+      # ("Crash when running without timezone info").
+      time.timeZone = "UTC";
     };
 
   testScript = ''
     machine.wait_for_unit("bazarr.service")
-    machine.wait_for_open_port(port)
+    machine.wait_for_open_port(${toString port})
     machine.succeed("curl --fail http://localhost:${toString port}/")
   '';
 })
diff --git a/nixos/tests/btrbk-section-order.nix b/nixos/tests/btrbk-section-order.nix
new file mode 100644
index 0000000000000..20f1afcf80ec7
--- /dev/null
+++ b/nixos/tests/btrbk-section-order.nix
@@ -0,0 +1,51 @@
+# This tests validates the order of generated sections that may contain
+# other sections.
+# When a `volume` section has both `subvolume` and `target` children,
+# `target` must go before `subvolume`. Otherwise, `target` will become
+# a child of the last `subvolume` instead of `volume`, due to the
+# order-sensitive config format.
+#
+# Issue: https://github.com/NixOS/nixpkgs/issues/195660
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "btrbk-section-order";
+  meta.maintainers = with lib.maintainers; [ oxalica ];
+
+  nodes.machine = { ... }: {
+    services.btrbk.instances.local = {
+      onCalendar = null;
+      settings = {
+        timestamp_format = "long";
+        target."ssh://global-target/".ssh_user = "root";
+        volume."/btrfs" = {
+          snapshot_dir = "/volume-snapshots";
+          target."ssh://volume-target/".ssh_user = "root";
+          subvolume."@subvolume" = {
+            snapshot_dir = "/subvolume-snapshots";
+            target."ssh://subvolume-target/".ssh_user = "root";
+          };
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("basic.target")
+    got = machine.succeed("cat /etc/btrbk/local.conf")
+    expect = """
+    backend btrfs-progs-sudo
+    timestamp_format long
+    target ssh://global-target/
+     ssh_user root
+    volume /btrfs
+     snapshot_dir /volume-snapshots
+     target ssh://volume-target/
+      ssh_user root
+     subvolume @subvolume
+      snapshot_dir /subvolume-snapshots
+      target ssh://subvolume-target/
+       ssh_user root
+    """.strip()
+    print(got)
+    assert got == expect
+  '';
+})
diff --git a/nixos/tests/chromium.nix b/nixos/tests/chromium.nix
index 6b296fe8a61ae..cdfdcc9bcdd2d 100644
--- a/nixos/tests/chromium.nix
+++ b/nixos/tests/chromium.nix
@@ -39,7 +39,9 @@ mapAttrs (channel: chromiumPkg: makeTest {
   name = "chromium-${channel}";
   meta = {
     maintainers = with maintainers; [ aszlig primeos ];
+  } // optionalAttrs (chromiumPkg.meta ? timeout) {
     # https://github.com/NixOS/hydra/issues/591#issuecomment-435125621
+    # Note: optionalAttrs is used since meta.timeout is not set for Google Chrome
     inherit (chromiumPkg.meta) timeout;
   };
 
@@ -65,6 +67,9 @@ mapAttrs (channel: chromiumPkg: makeTest {
     from contextlib import contextmanager
 
 
+    major_version = "${versions.major (getVersion chromiumPkg.name)}"
+
+
     # Run as user alice
     def ru(cmd):
         return "su - ${user} -c " + shlex.quote(cmd)
@@ -84,7 +89,6 @@ mapAttrs (channel: chromiumPkg: makeTest {
             binary = pname
         # Add optional CLI options:
         options = []
-        major_version = "${versions.major (getVersion chromiumPkg.name)}"
         if major_version > "95" and not pname.startswith("google-chrome"):
             # Workaround to avoid a GPU crash:
             options.append("--use-gl=swiftshader")
@@ -162,6 +166,8 @@ mapAttrs (channel: chromiumPkg: makeTest {
         clipboard = machine.succeed(
             ru("${pkgs.xclip}/bin/xclip -o")
         )
+        if url == "chrome://gpu":
+            clipboard = ""  # TODO: We cannot copy the text via Ctrl+a
         print(f"{description} window content:\n{clipboard}")
         with machine.nested(description):
             yield clipboard
@@ -242,9 +248,10 @@ mapAttrs (channel: chromiumPkg: makeTest {
         machine.screenshot("after_copy_from_chromium")
 
 
-    with test_new_win("gpu_info", "chrome://gpu", "chrome://gpu"):
+    with test_new_win("gpu_info", "chrome://gpu", "GPU Internals"):
         # To check the text rendering (catches regressions like #131074):
         machine.wait_for_text("Graphics Feature Status")
+        # TODO: Fix copying all of the text to the clipboard
 
 
     with test_new_win("version_info", "chrome://version", "About Version") as clipboard:
diff --git a/nixos/tests/cloud-init-hostname.nix b/nixos/tests/cloud-init-hostname.nix
new file mode 100644
index 0000000000000..7c657cc9f6f98
--- /dev/null
+++ b/nixos/tests/cloud-init-hostname.nix
@@ -0,0 +1,46 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  # Hostname can also be set through "hostname" in user-data.
+  # This is how proxmox configures hostname through cloud-init.
+  metadataDrive = pkgs.stdenv.mkDerivation {
+    name = "metadata";
+    buildCommand = ''
+      mkdir -p $out/iso
+
+      cat << EOF > $out/iso/user-data
+      #cloud-config
+      hostname: testhostname
+      EOF
+
+      cat << EOF > $out/iso/meta-data
+      instance-id: iid-local02
+      EOF
+
+      ${pkgs.cdrkit}/bin/genisoimage -volid cidata -joliet -rock -o $out/metadata.iso $out/iso
+    '';
+  };
+
+in makeTest {
+  name = "cloud-init-hostname";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ lewo illustris ];
+  };
+
+  nodes.machine2 = { ... }: {
+    virtualisation.qemu.options = [ "-cdrom" "${metadataDrive}/metadata.iso" ];
+    services.cloud-init.enable = true;
+    networking.hostName = "";
+  };
+
+  testScript = ''
+    unnamed.wait_for_unit("cloud-final.service")
+    assert "testhostname" in unnamed.succeed("hostname")
+  '';
+}
diff --git a/nixos/tests/cloud-init.nix b/nixos/tests/cloud-init.nix
index a86cd569e4215..786e01add7d4b 100644
--- a/nixos/tests/cloud-init.nix
+++ b/nixos/tests/cloud-init.nix
@@ -49,19 +49,17 @@ let
               gateway: '12.34.56.9'
           - type: nameserver
             address:
-            - '8.8.8.8'
+            - '6.7.8.9'
             search:
             - 'example.com'
       EOF
       ${pkgs.cdrkit}/bin/genisoimage -volid cidata -joliet -rock -o $out/metadata.iso $out/iso
       '';
   };
+
 in makeTest {
   name = "cloud-init";
-  meta = with pkgs.lib.maintainers; {
-    maintainers = [ lewo ];
-    broken = true; # almost always times out after spending many hours
-  };
+  meta.maintainers = with pkgs.lib.maintainers; [ lewo illustris ];
   nodes.machine = { ... }:
   {
     virtualisation.qemu.options = [ "-cdrom" "${metadataDrive}/metadata.iso" ];
@@ -90,21 +88,27 @@ in makeTest {
 
     # we should be able to log in as the root user, as well as the created nixos user
     unnamed.succeed(
-        "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/id_snakeoil root@localhost 'true'"
+        "timeout 10 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/id_snakeoil root@localhost 'true'"
     )
     unnamed.succeed(
-        "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/id_snakeoil nixos@localhost 'true'"
+        "timeout 10 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/id_snakeoil nixos@localhost 'true'"
     )
 
     # test changing hostname via cloud-init worked
     assert (
         unnamed.succeed(
-            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/id_snakeoil nixos@localhost 'hostname'"
+            "timeout 10 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/id_snakeoil nixos@localhost 'hostname'"
         ).strip()
         == "test"
     )
 
+    # check IP and route configs
     assert "default via 12.34.56.9 dev eth0 proto static" in unnamed.succeed("ip route")
     assert "12.34.56.0/24 dev eth0 proto kernel scope link src 12.34.56.78" in unnamed.succeed("ip route")
+
+    # check nameserver and search configs
+    assert "6.7.8.9" in unnamed.succeed("resolvectl status")
+    assert "example.com" in unnamed.succeed("resolvectl status")
+
   '';
 }
diff --git a/nixos/tests/containers-unified-hierarchy.nix b/nixos/tests/containers-unified-hierarchy.nix
new file mode 100644
index 0000000000000..978d59e12c8a1
--- /dev/null
+++ b/nixos/tests/containers-unified-hierarchy.nix
@@ -0,0 +1,21 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "containers-unified-hierarchy";
+  meta = {
+    maintainers = with lib.maintainers; [ farnoy ];
+  };
+
+  nodes.machine = { ... }: {
+    containers = {
+      test-container = {
+        autoStart = true;
+        config = { };
+      };
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("default.target")
+
+    machine.succeed("echo 'stat -fc %T /sys/fs/cgroup/ | grep cgroup2fs' | nixos-container root-login test-container")
+  '';
+})
diff --git a/nixos/tests/dhparams.nix b/nixos/tests/dhparams.nix
index a0de2911777c7..021042fafdb10 100644
--- a/nixos/tests/dhparams.nix
+++ b/nixos/tests/dhparams.nix
@@ -1,81 +1,69 @@
-let
-  common = { pkgs, ... }: {
-    security.dhparams.enable = true;
-    environment.systemPackages = [ pkgs.openssl ];
-  };
-
-in import ./make-test-python.nix {
+import ./make-test-python.nix {
   name = "dhparams";
 
-  nodes.generation1 = { pkgs, config, ... }: {
-    imports = [ common ];
-    security.dhparams.params = {
-      # Use low values here because we don't want the test to run for ages.
-      foo.bits = 16;
-      # Also use the old format to make sure the type is coerced in the right
-      # way.
-      bar = 17;
-    };
+  nodes.machine = { pkgs, ... }: {
+    security.dhparams.enable = true;
+    environment.systemPackages = [ pkgs.openssl ];
 
-    systemd.services.foo = {
-      description = "Check systemd Ordering";
-      wantedBy = [ "multi-user.target" ];
-      unitConfig = {
-        # This is to make sure that the dhparams generation of foo occurs
-        # before this service so we need this service to start as early as
-        # possible to provoke a race condition.
-        DefaultDependencies = false;
-
-        # We check later whether the service has been started or not.
-        ConditionPathExists = config.security.dhparams.params.foo.path;
+    specialisation = {
+      gen1.configuration = { config, ... }: {
+        security.dhparams.params = {
+          # Use low values here because we don't want the test to run for ages.
+          foo.bits = 1024;
+          # Also use the old format to make sure the type is coerced in the right
+          # way.
+          bar = 1025;
+        };
+
+        systemd.services.foo = {
+          description = "Check systemd Ordering";
+          wantedBy = [ "multi-user.target" ];
+          unitConfig = {
+            # This is to make sure that the dhparams generation of foo occurs
+            # before this service so we need this service to start as early as
+            # possible to provoke a race condition.
+            DefaultDependencies = false;
+
+            # We check later whether the service has been started or not.
+            ConditionPathExists = config.security.dhparams.params.foo.path;
+          };
+          serviceConfig.Type = "oneshot";
+          serviceConfig.RemainAfterExit = true;
+          # The reason we only provide an ExecStop here is to ensure that we don't
+          # accidentally trigger an error because a file system is not yet ready
+          # during very early startup (we might not even have the Nix store
+          # available, for example if future changes in NixOS use systemd mount
+          # units to do early file system initialisation).
+          serviceConfig.ExecStop = "${pkgs.coreutils}/bin/true";
+        };
+      };
+      gen2.configuration = {
+        security.dhparams.params.foo.bits = 1026;
+      };
+      gen3.configuration =  {};
+      gen4.configuration = {
+        security.dhparams.stateful = false;
+        security.dhparams.params.foo2.bits = 1027;
+        security.dhparams.params.bar2.bits = 1028;
+      };
+      gen5.configuration = {
+        security.dhparams.defaultBitSize = 1029;
+        security.dhparams.params.foo3 = {};
+        security.dhparams.params.bar3 = {};
       };
-      serviceConfig.Type = "oneshot";
-      serviceConfig.RemainAfterExit = true;
-      # The reason we only provide an ExecStop here is to ensure that we don't
-      # accidentally trigger an error because a file system is not yet ready
-      # during very early startup (we might not even have the Nix store
-      # available, for example if future changes in NixOS use systemd mount
-      # units to do early file system initialisation).
-      serviceConfig.ExecStop = "${pkgs.coreutils}/bin/true";
     };
   };
 
-  nodes.generation2 = {
-    imports = [ common ];
-    security.dhparams.params.foo.bits = 18;
-  };
-
-  nodes.generation3 = common;
-
-  nodes.generation4 = {
-    imports = [ common ];
-    security.dhparams.stateful = false;
-    security.dhparams.params.foo2.bits = 18;
-    security.dhparams.params.bar2.bits = 19;
-  };
-
-  nodes.generation5 = {
-    imports = [ common ];
-    security.dhparams.defaultBitSize = 30;
-    security.dhparams.params.foo3 = {};
-    security.dhparams.params.bar3 = {};
-  };
-
   testScript = { nodes, ... }: let
     getParamPath = gen: name: let
-      node = "generation${toString gen}";
-    in nodes.${node}.config.security.dhparams.params.${name}.path;
+      node = "gen${toString gen}";
+    in nodes.machine.config.specialisation.${node}.configuration.security.dhparams.params.${name}.path;
 
     switchToGeneration = gen: let
-      node = "generation${toString gen}";
-      inherit (nodes.${node}.config.system.build) toplevel;
-      switchCmd = "${toplevel}/bin/switch-to-configuration test";
+      switchCmd = "${nodes.machine.config.system.build.toplevel}/specialisation/gen${toString gen}/bin/switch-to-configuration test";
     in ''
       with machine.nested("switch to generation ${toString gen}"):
-          machine.succeed(
-              "${switchCmd}"
-          )
-          machine = ${node}
+        machine.succeed("${switchCmd}")
     '';
 
   in ''
@@ -92,22 +80,20 @@ in import ./make-test-python.nix {
             if match[1] != str(bits):
                 raise Exception(f"bit size should be {bits} but it is {match[1]} instead.")
 
-
-    machine = generation1
-
     machine.wait_for_unit("multi-user.target")
+    ${switchToGeneration 1}
 
     with subtest("verify startup order"):
         machine.succeed("systemctl is-active foo.service")
 
     with subtest("check bit sizes of dhparam files"):
-        assert_param_bits("${getParamPath 1 "foo"}", 16)
-        assert_param_bits("${getParamPath 1 "bar"}", 17)
+        assert_param_bits("${getParamPath 1 "foo"}", 1024)
+        assert_param_bits("${getParamPath 1 "bar"}", 1025)
 
     ${switchToGeneration 2}
 
     with subtest("check whether bit size has changed"):
-        assert_param_bits("${getParamPath 2 "foo"}", 18)
+        assert_param_bits("${getParamPath 2 "foo"}", 1026)
 
     with subtest("ensure that dhparams file for 'bar' was deleted"):
         machine.fail("test -e ${getParamPath 1 "bar"}")
@@ -115,16 +101,16 @@ in import ./make-test-python.nix {
     ${switchToGeneration 3}
 
     with subtest("ensure that 'security.dhparams.path' has been deleted"):
-        machine.fail("test -e ${nodes.generation3.config.security.dhparams.path}")
+        machine.fail("test -e ${nodes.machine.config.specialisation.gen3.configuration.security.dhparams.path}")
 
     ${switchToGeneration 4}
 
     with subtest("check bit sizes dhparam files"):
         assert_param_bits(
-            "${getParamPath 4 "foo2"}", 18
+            "${getParamPath 4 "foo2"}", 1027
         )
         assert_param_bits(
-            "${getParamPath 4 "bar2"}", 19
+            "${getParamPath 4 "bar2"}", 1028
         )
 
     with subtest("check whether dhparam files are in the Nix store"):
@@ -136,7 +122,7 @@ in import ./make-test-python.nix {
     ${switchToGeneration 5}
 
     with subtest("check whether defaultBitSize works as intended"):
-        assert_param_bits("${getParamPath 5 "foo3"}", 30)
-        assert_param_bits("${getParamPath 5 "bar3"}", 30)
+        assert_param_bits("${getParamPath 5 "foo3"}", 1029)
+        assert_param_bits("${getParamPath 5 "bar3"}", 1029)
   '';
 }
diff --git a/nixos/tests/dnscrypt-proxy2.nix b/nixos/tests/dnscrypt-proxy2.nix
index 1ba5d983e9b92..4435d77bbf3b2 100644
--- a/nixos/tests/dnscrypt-proxy2.nix
+++ b/nixos/tests/dnscrypt-proxy2.nix
@@ -1,4 +1,6 @@
-import ./make-test-python.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: let
+  localProxyPort = 43;
+in {
   name = "dnscrypt-proxy2";
   meta = with pkgs.lib.maintainers; {
     maintainers = [ joachifm ];
@@ -9,7 +11,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     # for a caching DNS client.
     client =
     { ... }:
-    let localProxyPort = 43; in
     {
       security.apparmor.enable = true;
 
@@ -32,5 +33,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   testScript = ''
     client.wait_for_unit("dnsmasq")
     client.wait_for_unit("dnscrypt-proxy2")
+    client.wait_until_succeeds("ss --numeric --udp --listening | grep -q ${toString localProxyPort}")
   '';
 })
diff --git a/nixos/tests/endlessh.nix b/nixos/tests/endlessh.nix
new file mode 100644
index 0000000000000..be742a749fdd8
--- /dev/null
+++ b/nixos/tests/endlessh.nix
@@ -0,0 +1,43 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+{
+  name = "endlessh";
+  meta.maintainers = with lib.maintainers; [ azahi ];
+
+  nodes = {
+    server = { ... }: {
+      services.endlessh = {
+        enable = true;
+        openFirewall = true;
+      };
+
+      specialisation = {
+        unprivileged.configuration.services.endlessh.port = 2222;
+
+        privileged.configuration.services.endlessh.port = 22;
+      };
+    };
+
+    client = { pkgs, ... }: {
+      environment.systemPackages = with pkgs; [ curl netcat ];
+    };
+  };
+
+  testScript = ''
+    def activate_specialisation(name: str):
+        server.succeed(f"/run/booted-system/specialisation/{name}/bin/switch-to-configuration test >&2")
+
+    start_all()
+
+    with subtest("Unprivileged"):
+        activate_specialisation("unprivileged")
+        server.wait_for_unit("endlessh.service")
+        server.wait_for_open_port(2222)
+        client.succeed("nc -dvW5 server 2222")
+
+    with subtest("Privileged"):
+        activate_specialisation("privileged")
+        server.wait_for_unit("endlessh.service")
+        server.wait_for_open_port(22)
+        client.succeed("nc -dvW5 server 22")
+  '';
+})
diff --git a/nixos/tests/fscrypt.nix b/nixos/tests/fscrypt.nix
new file mode 100644
index 0000000000000..03367979359bd
--- /dev/null
+++ b/nixos/tests/fscrypt.nix
@@ -0,0 +1,50 @@
+import ./make-test-python.nix ({ ... }:
+{
+  name = "fscrypt";
+
+  nodes.machine = { pkgs, ... }: {
+    imports = [ ./common/user-account.nix ];
+    security.pam.enableFscrypt = true;
+  };
+
+  testScript = ''
+    def login_as_alice():
+        machine.wait_until_tty_matches("1", "login: ")
+        machine.send_chars("alice\n")
+        machine.wait_until_tty_matches("1", "Password: ")
+        machine.send_chars("foobar\n")
+        machine.wait_until_tty_matches("1", "alice\@machine")
+
+
+    def logout():
+        machine.send_chars("logout\n")
+        machine.wait_until_tty_matches("1", "login: ")
+
+
+    machine.wait_for_unit("default.target")
+
+    with subtest("Enable fscrypt on filesystem"):
+        machine.succeed("tune2fs -O encrypt /dev/vda")
+        machine.succeed("fscrypt setup --quiet --force --time=1ms")
+
+    with subtest("Set up alice with an fscrypt-enabled home directory"):
+        machine.succeed("(echo foobar; echo foobar) | passwd alice")
+        machine.succeed("chown -R alice.users ~alice")
+        machine.succeed("echo foobar | fscrypt encrypt --skip-unlock --source=pam_passphrase --user=alice /home/alice")
+
+    with subtest("Create file as alice"):
+      login_as_alice()
+      machine.succeed("echo hello > /home/alice/world")
+      logout()
+      # Wait for logout to be processed
+      machine.sleep(1)
+
+    with subtest("File should not be readable without being logged in as alice"):
+      machine.fail("cat /home/alice/world")
+
+    with subtest("File should be readable again as alice"):
+      login_as_alice()
+      machine.succeed("cat /home/alice/world")
+      logout()
+  '';
+})
diff --git a/nixos/tests/garage.nix b/nixos/tests/garage.nix
new file mode 100644
index 0000000000000..dc1f83e7f8f3c
--- /dev/null
+++ b/nixos/tests/garage.nix
@@ -0,0 +1,169 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+let
+    mkNode = { replicationMode, publicV6Address ? "::1" }: { pkgs, ... }: {
+      networking.interfaces.eth1.ipv6.addresses = [{
+        address = publicV6Address;
+        prefixLength = 64;
+      }];
+
+      networking.firewall.allowedTCPPorts = [ 3901 3902 ];
+
+      services.garage = {
+        enable = true;
+        settings = {
+          replication_mode = replicationMode;
+
+          rpc_bind_addr = "[::]:3901";
+          rpc_public_addr = "[${publicV6Address}]:3901";
+          rpc_secret = "5c1915fa04d0b6739675c61bf5907eb0fe3d9c69850c83820f51b4d25d13868c";
+
+          s3_api = {
+            s3_region = "garage";
+            api_bind_addr = "[::]:3900";
+            root_domain = ".s3.garage";
+          };
+
+          s3_web = {
+            bind_addr = "[::]:3902";
+            root_domain = ".web.garage";
+            index = "index.html";
+          };
+        };
+      };
+      environment.systemPackages = [ pkgs.minio-client ];
+
+      # Garage requires at least 1GiB of free disk space to run.
+      virtualisation.diskSize = 2 * 1024;
+    };
+
+
+in {
+  name = "garage";
+  meta = {
+    maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
+  };
+
+  nodes = {
+    single_node = mkNode { replicationMode = "none"; };
+    node1 = mkNode { replicationMode = 3; publicV6Address = "fc00:1::1"; };
+    node2 = mkNode { replicationMode = 3; publicV6Address = "fc00:1::2"; };
+    node3 = mkNode { replicationMode = 3; publicV6Address = "fc00:1::3"; };
+    node4 = mkNode { replicationMode = 3; publicV6Address = "fc00:1::4"; };
+  };
+
+  testScript = ''
+    from typing import List
+    from dataclasses import dataclass
+    import re
+    start_all()
+
+    cur_version_regex = re.compile('Current cluster layout version: (?P<ver>\d*)')
+    key_creation_regex = re.compile('Key name: (?P<key_name>.*)\nKey ID: (?P<key_id>.*)\nSecret key: (?P<secret_key>.*)')
+
+    @dataclass
+    class S3Key:
+       key_name: str
+       key_id: str
+       secret_key: str
+
+    @dataclass
+    class GarageNode:
+       node_id: str
+       host: str
+
+    def get_node_fqn(machine: Machine) -> GarageNode:
+      node_id, host = machine.succeed("garage node id").split('@')
+      return GarageNode(node_id=node_id, host=host)
+
+    def get_node_id(machine: Machine) -> str:
+      return get_node_fqn(machine).node_id
+
+    def get_layout_version(machine: Machine) -> int:
+      version_data = machine.succeed("garage layout show")
+      m = cur_version_regex.search(version_data)
+      if m and m.group('ver') is not None:
+        return int(m.group('ver')) + 1
+      else:
+        raise ValueError('Cannot find current layout version')
+
+    def apply_garage_layout(machine: Machine, layouts: List[str]):
+       for layout in layouts:
+          machine.succeed(f"garage layout assign {layout}")
+       version = get_layout_version(machine)
+       machine.succeed(f"garage layout apply --version {version}")
+
+    def create_api_key(machine: Machine, key_name: str) -> S3Key:
+       output = machine.succeed(f"garage key new --name {key_name}")
+       m = key_creation_regex.match(output)
+       if not m or not m.group('key_id') or not m.group('secret_key'):
+          raise ValueError('Cannot parse API key data')
+       return S3Key(key_name=key_name, key_id=m.group('key_id'), secret_key=m.group('secret_key'))
+
+    def get_api_key(machine: Machine, key_pattern: str) -> S3Key:
+       output = machine.succeed(f"garage key info {key_pattern}")
+       m = key_creation_regex.match(output)
+       if not m or not m.group('key_name') or not m.group('key_id') or not m.group('secret_key'):
+           raise ValueError('Cannot parse API key data')
+       return S3Key(key_name=m.group('key_name'), key_id=m.group('key_id'), secret_key=m.group('secret_key'))
+
+    def test_bucket_writes(node):
+      node.succeed("garage bucket create test-bucket")
+      s3_key = create_api_key(node, "test-api-key")
+      node.succeed("garage bucket allow --read --write test-bucket --key test-api-key")
+      other_s3_key = get_api_key(node, 'test-api-key')
+      assert other_s3_key.secret_key == other_s3_key.secret_key
+      node.succeed(
+        f"mc alias set test-garage http://[::1]:3900 {s3_key.key_id} {s3_key.secret_key} --api S3v4"
+      )
+      node.succeed("echo test | mc pipe test-garage/test-bucket/test.txt")
+      assert node.succeed("mc cat test-garage/test-bucket/test.txt").strip() == "test"
+
+    def test_bucket_over_http(node, bucket='test-bucket', url=None):
+      if url is None:
+         url = f"{bucket}.web.garage"
+
+      node.succeed(f'garage bucket website --allow {bucket}')
+      node.succeed(f'echo hello world | mc pipe test-garage/{bucket}/index.html')
+      assert (node.succeed(f"curl -H 'Host: {url}' http://localhost:3902")).strip() == 'hello world'
+
+    with subtest("Garage works as a single-node S3 storage"):
+      single_node.wait_for_unit("garage.service")
+      single_node.wait_for_open_port(3900)
+      # Now Garage is initialized.
+      single_node_id = get_node_id(single_node)
+      apply_garage_layout(single_node, [f'-z qemutest -c 1 "{single_node_id}"'])
+      # Now Garage is operational.
+      test_bucket_writes(single_node)
+      test_bucket_over_http(single_node)
+
+    with subtest("Garage works as a multi-node S3 storage"):
+      nodes = ('node1', 'node2', 'node3', 'node4')
+      rev_machines = {m.name: m for m in machines}
+      def get_machine(key): return rev_machines[key]
+      for key in nodes:
+        node = get_machine(key)
+        node.wait_for_unit("garage.service")
+        node.wait_for_open_port(3900)
+
+      # Garage is initialized on all nodes.
+      node_ids = {key: get_node_fqn(get_machine(key)) for key in nodes}
+
+      for key in nodes:
+        for other_key in nodes:
+          if other_key != key:
+            other_id = node_ids[other_key]
+            get_machine(key).succeed(f"garage node connect {other_id.node_id}@{other_id.host}")
+
+      # Provide multiple zones for the nodes.
+      zones = ["nixcon", "nixcon", "paris_meetup", "fosdem"]
+      apply_garage_layout(node1,
+      [
+        f'{ndata.node_id} -z {zones[index]} -c 1'
+        for index, ndata in enumerate(node_ids.values())
+      ])
+      # Now Garage is operational.
+      test_bucket_writes(node1)
+      for node in nodes:
+         test_bucket_over_http(get_machine(node))
+  '';
+})
diff --git a/nixos/tests/grafana.nix b/nixos/tests/grafana/basic.nix
index 5364f0ca8b0b4..f6566d4497098 100644
--- a/nixos/tests/grafana.nix
+++ b/nixos/tests/grafana/basic.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ lib, pkgs, ... }:
+import ../make-test-python.nix ({ lib, pkgs, ... }:
 
 let
   inherit (lib) mkMerge nameValuePair maintainers;
@@ -6,23 +6,31 @@ let
   baseGrafanaConf = {
     services.grafana = {
       enable = true;
-      addr = "localhost";
-      analytics.reporting.enable = false;
-      domain = "localhost";
-      security = {
-        adminUser = "testadmin";
-        adminPassword = "snakeoilpwd";
+      settings = {
+        analytics.reporting_enabled = false;
+
+        server = {
+          http_addr = "localhost";
+          domain = "localhost";
+        };
+
+        security = {
+          admin_user = "testadmin";
+          admin_password = "snakeoilpwd";
+        };
       };
     };
   };
 
   extraNodeConfs = {
+    sqlite = {};
+
     declarativePlugins = {
       services.grafana.declarativePlugins = [ pkgs.grafanaPlugins.grafana-clock-panel ];
     };
 
     postgresql = {
-      services.grafana.database = {
+      services.grafana.settings.database = {
         host = "127.0.0.1:5432";
         user = "grafana";
       };
@@ -38,7 +46,7 @@ let
     };
 
     mysql = {
-      services.grafana.database.user = "grafana";
+      services.grafana.settings.database.user = "grafana";
       services.mysql = {
         enable = true;
         ensureDatabases = [ "grafana" ];
@@ -52,14 +60,9 @@ let
     };
   };
 
-  nodes = builtins.listToAttrs (map (dbName:
-    nameValuePair dbName (mkMerge [
-    baseGrafanaConf
-    (extraNodeConfs.${dbName} or {})
-  ])) [ "sqlite" "declarativePlugins" "postgresql" "mysql" ]);
-
+  nodes = builtins.mapAttrs (_: val: mkMerge [ val baseGrafanaConf ]) extraNodeConfs;
 in {
-  name = "grafana";
+  name = "grafana-basic";
 
   meta = with maintainers; {
     maintainers = [ willibutz ];
diff --git a/nixos/tests/grafana/default.nix b/nixos/tests/grafana/default.nix
new file mode 100644
index 0000000000000..9c26225718001
--- /dev/null
+++ b/nixos/tests/grafana/default.nix
@@ -0,0 +1,9 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../../.. { inherit system config; }
+}:
+
+{
+  basic = import ./basic.nix { inherit system pkgs; };
+  provision = import ./provision { inherit system pkgs; };
+}
diff --git a/nixos/tests/grafana/provision/contact-points.yaml b/nixos/tests/grafana/provision/contact-points.yaml
new file mode 100644
index 0000000000000..2a5f14e75e2db
--- /dev/null
+++ b/nixos/tests/grafana/provision/contact-points.yaml
@@ -0,0 +1,9 @@
+apiVersion: 1
+
+contactPoints:
+  - name: "Test Contact Point"
+    receivers:
+      - uid: "test_contact_point"
+        type: prometheus-alertmanager
+        settings:
+          url: http://localhost:9000
diff --git a/nixos/tests/grafana/provision/dashboards.yaml b/nixos/tests/grafana/provision/dashboards.yaml
new file mode 100644
index 0000000000000..dc83fe6b892dc
--- /dev/null
+++ b/nixos/tests/grafana/provision/dashboards.yaml
@@ -0,0 +1,6 @@
+apiVersion: 1
+
+providers:
+  - name: 'default'
+    options:
+      path: /var/lib/grafana/dashboards
diff --git a/nixos/tests/grafana/provision/datasources.yaml b/nixos/tests/grafana/provision/datasources.yaml
new file mode 100644
index 0000000000000..ccf9481db7f3f
--- /dev/null
+++ b/nixos/tests/grafana/provision/datasources.yaml
@@ -0,0 +1,7 @@
+apiVersion: 1
+
+datasources:
+  - name: 'Test Datasource'
+    type: 'testdata'
+    access: 'proxy'
+    uid: 'test_datasource'
diff --git a/nixos/tests/grafana/provision/default.nix b/nixos/tests/grafana/provision/default.nix
new file mode 100644
index 0000000000000..7a707ab9fed1f
--- /dev/null
+++ b/nixos/tests/grafana/provision/default.nix
@@ -0,0 +1,229 @@
+import ../../make-test-python.nix ({ lib, pkgs, ... }:
+
+let
+  inherit (lib) mkMerge nameValuePair maintainers;
+
+  baseGrafanaConf = {
+    services.grafana = {
+      enable = true;
+      provision.enable = true;
+      settings = {
+        analytics.reporting_enabled = false;
+
+        server = {
+          http_addr = "localhost";
+          domain = "localhost";
+        };
+
+        security = {
+          admin_user = "testadmin";
+          admin_password = "snakeoilpwd";
+        };
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "L /var/lib/grafana/dashboards/test.json 0700 grafana grafana - ${pkgs.writeText "test.json" (builtins.readFile ./test_dashboard.json)}"
+    ];
+  };
+
+  extraNodeConfs = {
+    provisionOld = {
+      services.grafana.provision = {
+        datasources = [{
+          name = "Test Datasource";
+          type = "testdata";
+          access = "proxy";
+          uid = "test_datasource";
+        }];
+
+        dashboards = [{ options.path = "/var/lib/grafana/dashboards"; }];
+
+        notifiers = [{
+          uid = "test_notifiers";
+          name = "Test Notifiers";
+          type = "email";
+          settings = {
+            singleEmail = true;
+            addresses = "test@test.com";
+          };
+        }];
+      };
+    };
+
+    provisionNix = {
+      services.grafana.provision = {
+        datasources.settings = {
+          apiVersion = 1;
+          datasources = [{
+            name = "Test Datasource";
+            type = "testdata";
+            access = "proxy";
+            uid = "test_datasource";
+          }];
+        };
+
+        dashboards.settings = {
+          apiVersion = 1;
+          providers = [{
+            name = "default";
+            options.path = "/var/lib/grafana/dashboards";
+          }];
+        };
+
+        alerting = {
+          rules.settings = {
+            groups = [{
+              name = "test_rule_group";
+              folder = "test_folder";
+              interval = "60s";
+              rules = [{
+                uid = "test_rule";
+                title = "Test Rule";
+                condition = "A";
+                data = [{
+                  refId = "A";
+                  datasourceUid = "-100";
+                  model = {
+                    conditions = [{
+                      evaluator = {
+                        params = [ 3 ];
+                        type = "git";
+                      };
+                      operator.type = "and";
+                      query.params = [ "A" ];
+                      reducer.type = "last";
+                      type = "query";
+                    }];
+                    datasource = {
+                      type = "__expr__";
+                      uid = "-100";
+                    };
+                    expression = "1==0";
+                    intervalMs = 1000;
+                    maxDataPoints = 43200;
+                    refId = "A";
+                    type = "math";
+                  };
+                }];
+                for = "60s";
+              }];
+            }];
+          };
+
+          contactPoints.settings = {
+            contactPoints = [{
+              name = "Test Contact Point";
+              receivers = [{
+                uid = "test_contact_point";
+                type = "prometheus-alertmanager";
+                settings.url = "http://localhost:9000";
+              }];
+            }];
+          };
+
+          policies.settings = {
+            policies = [{
+              receiver = "Test Contact Point";
+            }];
+          };
+
+          templates.settings = {
+            templates = [{
+              name = "Test Template";
+              template = "Test message";
+            }];
+          };
+
+          muteTimings.settings = {
+            muteTimes = [{
+              name = "Test Mute Timing";
+            }];
+          };
+        };
+      };
+    };
+
+    provisionYaml = {
+      services.grafana.provision = {
+        datasources.path = ./datasources.yaml;
+        dashboards.path = ./dashboards.yaml;
+        alerting = {
+          rules.path = ./rules.yaml;
+          contactPoints.path = ./contact-points.yaml;
+          policies.path = ./policies.yaml;
+          templates.path = ./templates.yaml;
+          muteTimings.path = ./mute-timings.yaml;
+        };
+      };
+    };
+  };
+
+  nodes = builtins.mapAttrs (_: val: mkMerge [ val baseGrafanaConf ]) extraNodeConfs;
+in {
+  name = "grafana-provision";
+
+  meta = with maintainers; {
+    maintainers = [ kfears willibutz ];
+  };
+
+  inherit nodes;
+
+  testScript = ''
+    start_all()
+
+    nodeOld = ("Nix (old format)", provisionOld)
+    nodeNix = ("Nix (new format)", provisionNix)
+    nodeYaml = ("Nix (YAML)", provisionYaml)
+
+    for nodeInfo in [nodeOld, nodeNix, nodeYaml]:
+        with subtest(f"Should start provision node: {nodeInfo[0]}"):
+            nodeInfo[1].wait_for_unit("grafana.service")
+            nodeInfo[1].wait_for_open_port(3000)
+
+        with subtest(f"Successful datasource provision with {nodeInfo[0]}"):
+            nodeInfo[1].succeed(
+                "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/datasources/uid/test_datasource | grep Test\ Datasource"
+            )
+
+        with subtest(f"Successful dashboard provision with {nodeInfo[0]}"):
+            nodeInfo[1].succeed(
+                "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/dashboards/uid/test_dashboard | grep Test\ Dashboard"
+            )
+
+
+
+    with subtest(f"Successful notifiers provision with {nodeOld[0]}"):
+        nodeOld[1].succeed(
+            "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/alert-notifications/uid/test_notifiers | grep Test\ Notifiers"
+        )
+
+
+
+    for nodeInfo in [nodeNix, nodeYaml]:
+        with subtest(f"Successful rule provision with {nodeInfo[0]}"):
+            nodeInfo[1].succeed(
+                "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/alert-rules/test_rule | grep Test\ Rule"
+            )
+
+        with subtest(f"Successful contact point provision with {nodeInfo[0]}"):
+            nodeInfo[1].succeed(
+                "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/contact-points | grep Test\ Contact\ Point"
+            )
+
+        with subtest(f"Successful policy provision with {nodeInfo[0]}"):
+            nodeInfo[1].succeed(
+                "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/policies | grep Test\ Contact\ Point"
+            )
+
+        with subtest(f"Successful template provision with {nodeInfo[0]}"):
+            nodeInfo[1].succeed(
+                "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/templates | grep Test\ Template"
+            )
+
+        with subtest("Successful mute timings provision with {nodeInfo[0]}"):
+            nodeInfo[1].succeed(
+                "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/mute-timings | grep Test\ Mute\ Timing"
+            )
+  '';
+})
diff --git a/nixos/tests/grafana/provision/mute-timings.yaml b/nixos/tests/grafana/provision/mute-timings.yaml
new file mode 100644
index 0000000000000..1f47f7c18f0c9
--- /dev/null
+++ b/nixos/tests/grafana/provision/mute-timings.yaml
@@ -0,0 +1,4 @@
+apiVersion: 1
+
+muteTimes:
+  - name: "Test Mute Timing"
diff --git a/nixos/tests/grafana/provision/policies.yaml b/nixos/tests/grafana/provision/policies.yaml
new file mode 100644
index 0000000000000..eb31126c4ba5c
--- /dev/null
+++ b/nixos/tests/grafana/provision/policies.yaml
@@ -0,0 +1,4 @@
+apiVersion: 1
+
+policies:
+  - receiver: "Test Contact Point"
diff --git a/nixos/tests/grafana/provision/rules.yaml b/nixos/tests/grafana/provision/rules.yaml
new file mode 100644
index 0000000000000..946539c8cb699
--- /dev/null
+++ b/nixos/tests/grafana/provision/rules.yaml
@@ -0,0 +1,36 @@
+apiVersion: 1
+
+groups:
+  - name: "test_rule_group"
+    folder: "test_group"
+    interval: 60s
+    rules:
+      - uid: "test_rule"
+        title: "Test Rule"
+        condition: A
+        data:
+          - refId: A
+            datasourceUid: '-100'
+            model:
+              conditions:
+                - evaluator:
+                    params:
+                      - 3
+                    type: gt
+                  operator:
+                    type: and
+                  query:
+                    params:
+                      - A
+                  reducer:
+                    type: last
+                  type: query
+              datasource:
+                type: __expr__
+                uid: '-100'
+              expression: 1==0
+              intervalMs: 1000
+              maxDataPoints: 43200
+              refId: A
+              type: math
+        for: 60s
diff --git a/nixos/tests/grafana/provision/templates.yaml b/nixos/tests/grafana/provision/templates.yaml
new file mode 100644
index 0000000000000..09df247b34513
--- /dev/null
+++ b/nixos/tests/grafana/provision/templates.yaml
@@ -0,0 +1,5 @@
+apiVersion: 1
+
+templates:
+  - name: "Test Template"
+    template: "Test message"
diff --git a/nixos/tests/grafana/provision/test_dashboard.json b/nixos/tests/grafana/provision/test_dashboard.json
new file mode 100644
index 0000000000000..6e7a5b37f22b8
--- /dev/null
+++ b/nixos/tests/grafana/provision/test_dashboard.json
@@ -0,0 +1,47 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": {
+          "type": "grafana",
+          "uid": "-- Grafana --"
+        },
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "target": {
+          "limit": 100,
+          "matchAny": false,
+          "tags": [],
+          "type": "dashboard"
+        },
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "fiscalYearStartMonth": 0,
+  "graphTooltip": 0,
+  "id": 28,
+  "links": [],
+  "liveNow": false,
+  "panels": [],
+  "schemaVersion": 37,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "timepicker": {},
+  "timezone": "",
+  "title": "Test Dashboard",
+  "uid": "test_dashboard",
+  "version": 1,
+  "weekStart": ""
+}
diff --git a/nixos/tests/installed-tests/default.nix b/nixos/tests/installed-tests/default.nix
index 2e38cd389c74a..78a6325a245ea 100644
--- a/nixos/tests/installed-tests/default.nix
+++ b/nixos/tests/installed-tests/default.nix
@@ -28,7 +28,7 @@ let
     , withX11 ? false
 
       # Extra flags to pass to gnome-desktop-testing-runner.
-    , testRunnerFlags ? ""
+    , testRunnerFlags ? []
 
       # Extra attributes to pass to makeTest.
       # They will be recursively merged into the attrset created by this function.
@@ -67,7 +67,7 @@ let
             '' +
             ''
               machine.succeed(
-                  "gnome-desktop-testing-runner ${testRunnerFlags} -d '${tested.installedTests}/share'"
+                  "gnome-desktop-testing-runner ${escapeShellArgs testRunnerFlags} -d '${tested.installedTests}/share'"
               )
             '';
         }
diff --git a/nixos/tests/installed-tests/flatpak-builder.nix b/nixos/tests/installed-tests/flatpak-builder.nix
index 41f4060fb69e5..d5e04fcf975ce 100644
--- a/nixos/tests/installed-tests/flatpak-builder.nix
+++ b/nixos/tests/installed-tests/flatpak-builder.nix
@@ -11,5 +11,5 @@ makeInstalledTest {
     virtualisation.diskSize = 2048;
   };
 
-  testRunnerFlags = "--timeout 3600";
+  testRunnerFlags = [ "--timeout" "3600" ];
 }
diff --git a/nixos/tests/installed-tests/flatpak.nix b/nixos/tests/installed-tests/flatpak.nix
index c7fe9cf458822..9524d890c4025 100644
--- a/nixos/tests/installed-tests/flatpak.nix
+++ b/nixos/tests/installed-tests/flatpak.nix
@@ -13,5 +13,5 @@ makeInstalledTest {
     virtualisation.diskSize = 3072;
   };
 
-  testRunnerFlags = "--timeout 3600";
+  testRunnerFlags = [ "--timeout" "3600" ];
 }
diff --git a/nixos/tests/installed-tests/gdk-pixbuf.nix b/nixos/tests/installed-tests/gdk-pixbuf.nix
index 3d0011a427a44..110efdbf710f2 100644
--- a/nixos/tests/installed-tests/gdk-pixbuf.nix
+++ b/nixos/tests/installed-tests/gdk-pixbuf.nix
@@ -9,5 +9,5 @@ makeInstalledTest {
     virtualisation.memorySize = if pkgs.stdenv.isi686 then 2047 else 4096;
   };
 
-  testRunnerFlags = "--timeout 1800";
+  testRunnerFlags = [ "--timeout" "1800" ];
 }
diff --git a/nixos/tests/invoiceplane.nix b/nixos/tests/invoiceplane.nix
index 4e63f8ac21c95..70ed96ee39f35 100644
--- a/nixos/tests/invoiceplane.nix
+++ b/nixos/tests/invoiceplane.nix
@@ -13,12 +13,12 @@ import ./make-test-python.nix ({ pkgs, ... }:
       services.invoiceplane.webserver = "caddy";
       services.invoiceplane.sites = {
         "site1.local" = {
-          #database.name = "invoiceplane1";
+          database.name = "invoiceplane1";
           database.createLocally = true;
           enable = true;
         };
         "site2.local" = {
-          #database.name = "invoiceplane2";
+          database.name = "invoiceplane2";
           database.createLocally = true;
           enable = true;
         };
@@ -46,37 +46,37 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
         with subtest("Finish InvoicePlane setup"):
           machine.succeed(
-            f"curl -sSfL --cookie-jar cjar {site_name}/index.php/setup/language"
+            f"curl -sSfL --cookie-jar cjar {site_name}/setup/language"
           )
           csrf_token = machine.succeed(
             "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'"
           )
           machine.succeed(
-            f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&ip_lang=english&btn_continue=Continue' {site_name}/index.php/setup/language"
+            f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&ip_lang=english&btn_continue=Continue' {site_name}/setup/language"
           )
           csrf_token = machine.succeed(
             "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'"
           )
           machine.succeed(
-            f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/index.php/setup/prerequisites"
+            f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/setup/prerequisites"
           )
           csrf_token = machine.succeed(
             "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'"
           )
           machine.succeed(
-            f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/index.php/setup/configure_database"
+            f"curl -sSfL --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/setup/configure_database"
           )
           csrf_token = machine.succeed(
             "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'"
           )
           machine.succeed(
-            f"curl -sSfl --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/index.php/setup/install_tables"
+            f"curl -sSfl --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/setup/install_tables"
           )
           csrf_token = machine.succeed(
             "grep ip_csrf_cookie cjar | cut -f 7 | tr -d '\n'"
           )
           machine.succeed(
-            f"curl -sSfl --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/index.php/setup/upgrade_tables"
+            f"curl -sSfl --cookie cjar --cookie-jar cjar -d '_ip_csrf={csrf_token}&btn_continue=Continue' {site_name}/setup/upgrade_tables"
           )
   '';
 })
diff --git a/nixos/tests/jenkins.nix b/nixos/tests/jenkins.nix
index 3f111426db388..a1ede6dc917bf 100644
--- a/nixos/tests/jenkins.nix
+++ b/nixos/tests/jenkins.nix
@@ -18,8 +18,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
           enable = true;
           jobBuilder = {
             enable = true;
-            accessUser = "admin";
-            accessTokenFile = "/var/lib/jenkins/secrets/initialAdminPassword";
             nixJobs = [
               { job = {
                   name = "job-1";
diff --git a/nixos/tests/jibri.nix b/nixos/tests/jibri.nix
index 223120cdb2291..45e30af9a9a55 100644
--- a/nixos/tests/jibri.nix
+++ b/nixos/tests/jibri.nix
@@ -35,9 +35,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     machine.wait_for_unit("jibri.service")
 
     machine.wait_until_succeeds(
-        "journalctl -b -u jitsi-videobridge2 -o cat | grep -q 'Performed a successful health check'", timeout=30
-    )
-    machine.wait_until_succeeds(
         "journalctl -b -u prosody -o cat | grep -q 'Authenticated as focus@auth.machine'", timeout=31
     )
     machine.wait_until_succeeds(
diff --git a/nixos/tests/k3s/multi-node.nix b/nixos/tests/k3s/multi-node.nix
index ce7e4b6ead148..9a6c7fd465739 100644
--- a/nixos/tests/k3s/multi-node.nix
+++ b/nixos/tests/k3s/multi-node.nix
@@ -54,15 +54,15 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
           role = "server";
           package = pkgs.k3s;
           clusterInit = true;
-          extraFlags = ''
-            --disable coredns \
-            --disable local-storage \
-            --disable metrics-server \
-            --disable servicelb \
-            --disable traefik \
-            --node-ip 192.168.1.1 \
-            --pause-image test.local/pause:local
-          '';
+          extraFlags = builtins.toString [
+            "--disable" "coredns"
+            "--disable" "local-storage"
+            "--disable" "metrics-server"
+            "--disable" "servicelb"
+            "--disable" "traefik"
+            "--node-ip" "192.168.1.1"
+            "--pause-image" "test.local/pause:local"
+          ];
         };
         networking.firewall.allowedTCPPorts = [ 2379 2380 6443 ];
         networking.firewall.allowedUDPPorts = [ 8472 ];
@@ -84,15 +84,15 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
           enable = true;
           serverAddr = "https://192.168.1.1:6443";
           clusterInit = false;
-          extraFlags = ''
-            --disable coredns \
-            --disable local-storage \
-            --disable metrics-server \
-            --disable servicelb \
-            --disable traefik \
-            --node-ip 192.168.1.3 \
-            --pause-image test.local/pause:local
-          '';
+          extraFlags = builtins.toString [
+            "--disable" "coredns"
+            "--disable" "local-storage"
+            "--disable" "metrics-server"
+            "--disable" "servicelb"
+            "--disable" "traefik"
+            "--node-ip" "192.168.1.3"
+            "--pause-image" "test.local/pause:local"
+          ];
         };
         networking.firewall.allowedTCPPorts = [ 2379 2380 6443 ];
         networking.firewall.allowedUDPPorts = [ 8472 ];
@@ -112,7 +112,10 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
           enable = true;
           role = "agent";
           serverAddr = "https://192.168.1.3:6443";
-          extraFlags = "--pause-image test.local/pause:local --node-ip 192.168.1.2";
+          extraFlags = lib.concatStringsSep " " [
+            "--pause-image" "test.local/pause:local"
+            "--node-ip" "192.168.1.2"
+          ];
         };
         networking.firewall.allowedTCPPorts = [ 6443 ];
         networking.firewall.allowedUDPPorts = [ 8472 ];
@@ -135,12 +138,15 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
           m.start()
           m.wait_for_unit("k3s")
 
+      is_aarch64 = "${toString pkgs.stdenv.isAarch64}" == "1"
+
       # wait for the agent to show up
       server.wait_until_succeeds("k3s kubectl get node agent")
 
       for m in machines:
-      '' # Fix-Me: Tests fail for 'aarch64-linux' as: "CONFIG_CGROUP_FREEZER: missing (fail)"
-      + lib.optionalString (!pkgs.stdenv.isAarch64) ''m.succeed("k3s check-config")'' + ''
+          # Fix-Me: Tests fail for 'aarch64-linux' as: "CONFIG_CGROUP_FREEZER: missing (fail)"
+          if not is_aarch64:
+              m.succeed("k3s check-config")
           m.succeed(
               "${pauseImage} | k3s ctr image import -"
           )
diff --git a/nixos/tests/k3s/single-node.nix b/nixos/tests/k3s/single-node.nix
index ab562500f5d21..a95fa4a031e3f 100644
--- a/nixos/tests/k3s/single-node.nix
+++ b/nixos/tests/k3s/single-node.nix
@@ -40,15 +40,14 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
       services.k3s.role = "server";
       services.k3s.package = pkgs.k3s;
       # Slightly reduce resource usage
-      services.k3s.extraFlags = ''
-        --disable coredns \
-        --disable local-storage \
-        --disable metrics-server \
-        --disable servicelb \
-        --disable traefik \
-        --pause-image \
-        test.local/pause:local
-      '';
+      services.k3s.extraFlags = builtins.toString [
+        "--disable" "coredns"
+        "--disable" "local-storage"
+        "--disable" "metrics-server"
+        "--disable" "servicelb"
+        "--disable" "traefik"
+        "--pause-image" "test.local/pause:local"
+      ];
 
       users.users = {
         noprivs = {
diff --git a/nixos/tests/kafka.nix b/nixos/tests/kafka.nix
index 5def759ca24d9..79af02710c32b 100644
--- a/nixos/tests/kafka.nix
+++ b/nixos/tests/kafka.nix
@@ -50,7 +50,7 @@ let
 
       kafka.wait_until_succeeds(
           "${kafkaPackage}/bin/kafka-topics.sh --create "
-          + "--zookeeper zookeeper1:2181 --partitions 1 "
+          + "--bootstrap-server localhost:9092 --partitions 1 "
           + "--replication-factor 1 --topic testtopic"
       )
       kafka.succeed(
@@ -58,22 +58,19 @@ let
           + "${kafkaPackage}/bin/kafka-console-producer.sh "
           + "--broker-list localhost:9092 --topic testtopic"
       )
-    '' + (if name == "kafka_0_9" then ''
-      assert "test 1" in kafka.succeed(
-          "${kafkaPackage}/bin/kafka-console-consumer.sh "
-          + "--zookeeper zookeeper1:2181 --topic testtopic "
-          + "--from-beginning --max-messages 1"
-      )
-    '' else ''
       assert "test 1" in kafka.succeed(
           "${kafkaPackage}/bin/kafka-console-consumer.sh "
           + "--bootstrap-server localhost:9092 --topic testtopic "
           + "--from-beginning --max-messages 1"
       )
-    '');
+    '';
   }) { inherit system; });
 
 in with pkgs; {
-  kafka_2_7  = makeKafkaTest "kafka_2_7"  apacheKafka_2_7;
   kafka_2_8  = makeKafkaTest "kafka_2_8"  apacheKafka_2_8;
+  kafka_3_0  = makeKafkaTest "kafka_3_0"  apacheKafka_3_0;
+  kafka_3_1  = makeKafkaTest "kafka_3_1"  apacheKafka_3_1;
+  kafka_3_2  = makeKafkaTest "kafka_3_2"  apacheKafka_3_2;
+  kafka_3_3  = makeKafkaTest "kafka_3_3"  apacheKafka_3_3;
+  kafka  = makeKafkaTest "kafka"  apacheKafka;
 }
diff --git a/nixos/tests/karma.nix b/nixos/tests/karma.nix
new file mode 100644
index 0000000000000..5ac2983b8aa3e
--- /dev/null
+++ b/nixos/tests/karma.nix
@@ -0,0 +1,84 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "karma";
+  nodes = {
+    server = { ... }: {
+      services.prometheus.alertmanager = {
+        enable = true;
+        logLevel = "debug";
+        port = 9093;
+        openFirewall = true;
+        configuration = {
+          global = {
+            resolve_timeout = "1m";
+          };
+          route = {
+            # Root route node
+            receiver = "test";
+            group_by = ["..."];
+            continue = false;
+            group_wait = "1s";
+            group_interval="15s";
+            repeat_interval = "24h";
+          };
+          receivers = [
+            {
+              name = "test";
+              webhook_configs = [
+                {
+                  url = "http://localhost:1234";
+                  send_resolved = true;
+                  max_alerts = 0;
+                }
+              ];
+            }
+          ];
+        };
+      };
+      services.karma = {
+        enable = true;
+        openFirewall = true;
+        settings = {
+          listen = {
+            address = "0.0.0.0";
+            port = 8081;
+          };
+          alertmanager = {
+            servers = [
+              {
+                name = "alertmanager";
+                uri = "https://127.0.0.1:9093";
+              }
+            ];
+          };
+          karma.name = "test-dashboard";
+          log.config = true;
+          log.requests = true;
+          log.timestamp = true;
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    with subtest("Wait for server to come up"):
+
+      server.wait_for_unit("alertmanager.service")
+      server.wait_for_unit("karma.service")
+
+      server.sleep(5) # wait for both services to settle
+
+      server.wait_for_open_port(9093)
+      server.wait_for_open_port(8081)
+
+    with subtest("Test alertmanager readiness"):
+      server.succeed("curl -s http://127.0.0.1:9093/-/ready")
+
+      # Karma only starts serving the dashboard once it has established connectivity to all alertmanagers in its config
+      # Therefore, this will fail if karma isn't able to reach alertmanager
+      server.succeed("curl -s http://127.0.0.1:8081")
+
+    server.shutdown()
+  '';
+})
diff --git a/nixos/tests/kernel-generic.nix b/nixos/tests/kernel-generic.nix
index 452c15a3a058d..7ee734a1eff05 100644
--- a/nixos/tests/kernel-generic.nix
+++ b/nixos/tests/kernel-generic.nix
@@ -30,7 +30,7 @@ let
       linux_5_4_hardened
       linux_5_10_hardened
       linux_5_15_hardened
-      linux_5_19_hardened
+      linux_6_0_hardened
 
       linux_testing;
   };
diff --git a/nixos/tests/kubo.nix b/nixos/tests/kubo.nix
index e84a873a1a18d..94aa24a9204fe 100644
--- a/nixos/tests/kubo.nix
+++ b/nixos/tests/kubo.nix
@@ -9,7 +9,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
       enable = true;
       # Also will add a unix domain socket socket API address, see module.
       startWhenNeeded = true;
-      apiAddress = "/ip4/127.0.0.1/tcp/2324";
+      settings.Addresses.API = "/ip4/127.0.0.1/tcp/2324";
       dataDir = "/mnt/ipfs";
     };
   };
@@ -17,7 +17,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
   nodes.fuse = { ... }: {
     services.kubo = {
       enable = true;
-      apiAddress = "/ip4/127.0.0.1/tcp/2324";
+      settings.Addresses.API = "/ip4/127.0.0.1/tcp/2324";
       autoMount = true;
     };
   };
diff --git a/nixos/tests/libvirtd.nix b/nixos/tests/libvirtd.nix
index ce122682da73e..49258fcb93eaf 100644
--- a/nixos/tests/libvirtd.nix
+++ b/nixos/tests/libvirtd.nix
@@ -26,7 +26,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   in ''
     start_all()
 
-    virthost.wait_for_unit("sockets.target")
+    virthost.wait_for_unit("multi-user.target")
 
     with subtest("enable default network"):
       virthost.succeed("virsh net-start default")
@@ -46,13 +46,16 @@ import ./make-test-python.nix ({ pkgs, ... }: {
       virthost.succeed("virsh pool-start zfs_storagepool")
       virthost.succeed("virsh vol-create-as zfs_storagepool disk1 25MB")
 
-    with subtest("check if nixos install iso boots and network works"):
+    with subtest("check if nixos install iso boots, network and autostart works"):
       virthost.succeed(
-        "virt-install -n nixos --osinfo=nixos-unstable --ram=1024 --graphics=none --disk=`find ${nixosInstallISO}/iso -type f | head -n1`,readonly=on --import --noautoconsole"
+        "virt-install -n nixos --osinfo nixos-unstable --memory 1024 --graphics none --disk `find ${nixosInstallISO}/iso -type f | head -n1`,readonly=on --import --noautoconsole --autostart"
       )
       virthost.succeed("virsh domstate nixos | grep running")
       virthost.wait_until_succeeds("ping -c 1 nixos")
       virthost.succeed("virsh ${virshShutdownCmd} nixos")
       virthost.wait_until_succeeds("virsh domstate nixos | grep 'shut off'")
+      virthost.shutdown()
+      virthost.wait_for_unit("multi-user.target")
+      virthost.wait_until_succeeds("ping -c 1 nixos")
   '';
 })
diff --git a/nixos/tests/logrotate.nix b/nixos/tests/logrotate.nix
index b0685f3af9ff1..94f6ad5103fb2 100644
--- a/nixos/tests/logrotate.nix
+++ b/nixos/tests/logrotate.nix
@@ -64,29 +64,6 @@ import ./make-test-python.nix ({ pkgs, ... }: rec {
           notifempty = true;
         };
       };
-      # extraConfig compatibility - should be added to top level, early.
-      services.logrotate.extraConfig = ''
-        nomail
-      '';
-      # paths compatibility
-      services.logrotate.paths = {
-        compat_path = {
-          path = "compat_test_path";
-        };
-        # user/group should be grouped as 'su user group'
-        compat_user = {
-          user = config.users.users.root.name;
-          group = "root";
-        };
-        # extraConfig in path should be added to block
-        compat_extraConfig = {
-          extraConfig = "dateext";
-        };
-        # keep -> rotate
-        compat_keep = {
-          keep = 1;
-        };
-      };
     };
   };
 
@@ -127,12 +104,6 @@ import ./make-test-python.nix ({ pkgs, ... }: rec {
               "sed -ne '/\"postrotate\" {/,/}/p' /tmp/logrotate.conf | grep endscript",
               "grep '\"file1\"\n\"file2\" {' /tmp/logrotate.conf",
               "sed -ne '/\"import\" {/,/}/p' /tmp/logrotate.conf | grep noolddir",
-              "sed -ne '1,/^\"/p' /tmp/logrotate.conf | grep nomail",
-              "grep '\"compat_test_path\" {' /tmp/logrotate.conf",
-              "sed -ne '/\"compat_user\" {/,/}/p' /tmp/logrotate.conf | grep 'su root root'",
-              "sed -ne '/\"compat_extraConfig\" {/,/}/p' /tmp/logrotate.conf | grep dateext",
-              "[[ $(sed -ne '/\"compat_keep\" {/,/}/p' /tmp/logrotate.conf | grep -w rotate) = \"  rotate 1\" ]]",
-              "! sed -ne '/\"compat_keep\" {/,/}/p' /tmp/logrotate.conf | grep -w keep",
           )
           # also check configFile option
           failingMachine.succeed(
diff --git a/nixos/tests/lxd-image-server.nix b/nixos/tests/lxd-image-server.nix
index 072f4570c2c9f..e5a292b61bd97 100644
--- a/nixos/tests/lxd-image-server.nix
+++ b/nixos/tests/lxd-image-server.nix
@@ -8,8 +8,8 @@ let
     };
   };
 
-  lxd-image-metadata = lxd-image.lxdMeta.${pkgs.system};
-  lxd-image-rootfs = lxd-image.lxdImage.${pkgs.system};
+  lxd-image-metadata = lxd-image.lxdMeta.${pkgs.stdenv.hostPlatform.system};
+  lxd-image-rootfs = lxd-image.lxdImage.${pkgs.stdenv.hostPlatform.system};
 
 in {
   name = "lxd-image-server";
diff --git a/nixos/tests/lxd.nix b/nixos/tests/lxd.nix
index 15d16564d641c..2c2c19e0eecf7 100644
--- a/nixos/tests/lxd.nix
+++ b/nixos/tests/lxd.nix
@@ -11,8 +11,8 @@ let
     };
   };
 
-  lxd-image-metadata = lxd-image.lxdMeta.${pkgs.system};
-  lxd-image-rootfs = lxd-image.lxdImage.${pkgs.system};
+  lxd-image-metadata = lxd-image.lxdMeta.${pkgs.stdenv.hostPlatform.system};
+  lxd-image-rootfs = lxd-image.lxdImage.${pkgs.stdenv.hostPlatform.system};
 
 in {
   name = "lxd";
@@ -23,7 +23,7 @@ in {
 
   nodes.machine = { lib, ... }: {
     virtualisation = {
-      diskSize = 2048;
+      diskSize = 4096;
 
       # Since we're testing `limits.cpu`, we've gotta have a known number of
       # cores to lean on
diff --git a/nixos/tests/merecat.nix b/nixos/tests/merecat.nix
new file mode 100644
index 0000000000000..9d8f66165ee90
--- /dev/null
+++ b/nixos/tests/merecat.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "merecat";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  nodes.machine = { config, pkgs, ... }: {
+    services.merecat = {
+      enable = true;
+      settings = {
+        hostname = "localhost";
+        virtual-host = true;
+        directory = toString (pkgs.runCommand "merecat-webdir" {} ''
+          mkdir -p $out/foo.localhost $out/bar.localhost
+          echo '<h1>Hello foo</h1>' > $out/foo.localhost/index.html
+          echo '<h1>Hello bar</h1>' > $out/bar.localhost/index.html
+        '');
+      };
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("merecat")
+    machine.wait_for_open_port(80)
+    machine.succeed("curl --fail foo.localhost | grep 'Hello foo'")
+    machine.succeed("curl --fail bar.localhost | grep 'Hello bar'")
+  '';
+})
diff --git a/nixos/tests/mosquitto.nix b/nixos/tests/mosquitto.nix
index d516d3373d9f6..70eecc89278b3 100644
--- a/nixos/tests/mosquitto.nix
+++ b/nixos/tests/mosquitto.nix
@@ -4,7 +4,7 @@ let
   port = 1888;
   tlsPort = 1889;
   anonPort = 1890;
-  bindTestPort = 1891;
+  bindTestPort = 18910;
   password = "VERY_secret";
   hashedPassword = "$7$101$/WJc4Mp+I+uYE9sR$o7z9rD1EYXHPwEP5GqQj6A7k4W1yVbePlb8TqNcuOLV9WNCiDgwHOB0JHC1WCtdkssqTBduBNUnUGd6kmZvDSw==";
   topic = "test/foo";
@@ -165,6 +165,10 @@ in {
         for t in threads: t.start()
         for t in threads: t.join()
 
+    def wait_uuid(uuid):
+        server.wait_for_console_text(uuid)
+        return None
+
 
     start_all()
     server.wait_for_unit("mosquitto.service")
@@ -203,14 +207,14 @@ in {
         parallel(
             lambda: client1.succeed(subscribe("-i 3688cdd7-aa07-42a4-be22-cb9352917e40", "reader")),
             lambda: [
-                server.wait_for_console_text("3688cdd7-aa07-42a4-be22-cb9352917e40"),
+                wait_uuid("3688cdd7-aa07-42a4-be22-cb9352917e40"),
                 client2.succeed(publish("-m test", "writer"))
             ])
 
         parallel(
             lambda: client1.fail(subscribe("-i 24ff16a2-ae33-4a51-9098-1b417153c712", "reader")),
             lambda: [
-                server.wait_for_console_text("24ff16a2-ae33-4a51-9098-1b417153c712"),
+                wait_uuid("24ff16a2-ae33-4a51-9098-1b417153c712"),
                 client2.succeed(publish("-m test", "reader"))
             ])
 
@@ -229,7 +233,7 @@ in {
             lambda: client1.succeed(subscribe("-i fd56032c-d9cb-4813-a3b4-6be0e04c8fc3",
                 "anonReader", port=${toString anonPort})),
             lambda: [
-                server.wait_for_console_text("fd56032c-d9cb-4813-a3b4-6be0e04c8fc3"),
+                wait_uuid("fd56032c-d9cb-4813-a3b4-6be0e04c8fc3"),
                 client2.succeed(publish("-m test", "anonWriter", port=${toString anonPort}))
             ])
   '';
diff --git a/nixos/tests/mysql/common.nix b/nixos/tests/mysql/common.nix
index 040d360b6d99c..7fdf0f33d3f37 100644
--- a/nixos/tests/mysql/common.nix
+++ b/nixos/tests/mysql/common.nix
@@ -1,10 +1,7 @@
 { lib, pkgs }: {
-  mariadbPackages = lib.filterAttrs (n: _: lib.hasPrefix "mariadb" n) (pkgs.callPackage ../../../pkgs/servers/sql/mariadb {
-    inherit (pkgs.darwin) cctools;
-    inherit (pkgs.darwin.apple_sdk.frameworks) CoreServices;
-  });
+  mariadbPackages = lib.filterAttrs (n: _: lib.hasPrefix "mariadb" n) (import ../../../pkgs/servers/sql/mariadb pkgs);
   mysqlPackages = {
-    inherit (pkgs) mysql57 mysql80;
+    inherit (pkgs) mysql80;
   };
   mkTestName = pkg: "mariadb_${builtins.replaceStrings ["."] [""] (lib.versions.majorMinor pkg.version)}";
 }
diff --git a/nixos/tests/nextcloud/default.nix b/nixos/tests/nextcloud/default.nix
index 9e378fe6a52d3..7dbdff9882387 100644
--- a/nixos/tests/nextcloud/default.nix
+++ b/nixos/tests/nextcloud/default.nix
@@ -22,4 +22,4 @@ foldl
     };
   })
 { }
-  [ 23 24 ]
+  [ 24 25 ]
diff --git a/nixos/tests/nginx-njs.nix b/nixos/tests/nginx-njs.nix
new file mode 100644
index 0000000000000..72be16384f1b3
--- /dev/null
+++ b/nixos/tests/nginx-njs.nix
@@ -0,0 +1,27 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "nginx-njs";
+
+  nodes.machine = { config, lib, pkgs, ... }: {
+    services.nginx = {
+      enable = true;
+      additionalModules = [ pkgs.nginxModules.njs ];
+      commonHttpConfig = ''
+        js_import http from ${builtins.toFile "http.js" ''
+          function hello(r) {
+              r.return(200, "Hello world!");
+          }
+          export default {hello};
+        ''};
+      '';
+      virtualHosts."localhost".locations."/".extraConfig = ''
+        js_content http.hello;
+      '';
+    };
+  };
+  testScript = ''
+    machine.wait_for_unit("nginx")
+
+    response = machine.wait_until_succeeds("curl -fvvv -s http://127.0.0.1/")
+    assert "Hello world!" == response, f"Expected 'Hello world!', got '{response}'"
+  '';
+})
diff --git a/nixos/tests/nscd.nix b/nixos/tests/nscd.nix
index 7bb6d90c3d4e0..1922812ef8c89 100644
--- a/nixos/tests/nscd.nix
+++ b/nixos/tests/nscd.nix
@@ -21,10 +21,31 @@ in
       192.0.2.1 somehost.test
     '';
 
+    systemd.services.sockdump = {
+      wantedBy = [ "multi-user.target" ];
+      path = [
+        # necessary for bcc to unpack kernel headers and invoke modprobe
+        pkgs.gnutar
+        pkgs.xz.bin
+        pkgs.kmod
+      ];
+      environment.PYTHONUNBUFFERED = "1";
+
+      serviceConfig = {
+        ExecStart = "${pkgs.sockdump}/bin/sockdump /var/run/nscd/socket";
+        Restart = "on-failure";
+        RestartSec = "1";
+        Type = "simple";
+      };
+    };
+
     specialisation = {
       withUnscd.configuration = { ... }: {
         services.nscd.package = pkgs.unscd;
       };
+      withNsncd.configuration = { ... }: {
+        services.nscd.enableNsncd = true;
+      };
     };
   };
 
@@ -40,9 +61,10 @@ in
                   "systemd-run --pty --property=Type=oneshot --property=DynamicUser=yes --property=User=iamatest whoami"
               )
 
-      # Test resolution of somehost.test with getent', to make sure we go via nscd
+      # Test resolution of somehost.test with getent', to make sure we go via
+      # nscd protocol
       def test_host_lookups():
-          with subtest("host lookups via nscd"):
+          with subtest("host lookups via nscd protocol"):
               # ahosts
               output = machine.succeed("${getent'} ahosts somehost.test")
               assert "192.0.2.1" in output
@@ -62,6 +84,7 @@ in
               assert "somehost.test" in machine.succeed("${getent'} hosts 2001:db8::1")
               assert "somehost.test" in machine.succeed("${getent'} hosts 192.0.2.1")
 
+
       # Test host resolution via nss modules works
       # We rely on nss-myhostname in this case, which resolves *.localhost and
       # _gateway.
@@ -87,6 +110,9 @@ in
       start_all()
       machine.wait_for_unit("default.target")
 
+      # give sockdump some time to finish attaching.
+      machine.sleep(5)
+
       # Test all tests with glibc-nscd.
       test_dynamic_user()
       test_host_lookups()
@@ -103,5 +129,13 @@ in
 
           # known to fail, unscd doesn't load external NSS modules
           # test_nss_myhostname()
+
+      with subtest("nsncd"):
+          machine.succeed('${specialisations}/withNsncd/bin/switch-to-configuration test')
+          machine.wait_for_unit("default.target")
+
+          test_dynamic_user()
+          test_host_lookups()
+          test_nss_myhostname()
     '';
 })
diff --git a/nixos/tests/ntfy-sh.nix b/nixos/tests/ntfy-sh.nix
new file mode 100644
index 0000000000000..c0c289b904b6e
--- /dev/null
+++ b/nixos/tests/ntfy-sh.nix
@@ -0,0 +1,20 @@
+import ./make-test-python.nix {
+
+  nodes.machine = { ... }: {
+    services.ntfy-sh.enable = true;
+  };
+
+  testScript = ''
+    import json
+
+    msg = "Test notification"
+
+    machine.wait_for_unit("multi-user.target")
+
+    machine.succeed(f"curl -d '{msg}' localhost:80/test")
+
+    notif = json.loads(machine.succeed("curl -s localhost:80/test/json?poll=1"))
+
+    assert msg == notif["message"], "Wrong message"
+  '';
+}
diff --git a/nixos/tests/podman/default.nix b/nixos/tests/podman/default.nix
index 67c7823c5a316..106ba2057d063 100644
--- a/nixos/tests/podman/default.nix
+++ b/nixos/tests/podman/default.nix
@@ -1,5 +1,3 @@
-# This test runs podman and checks if simple container starts
-
 import ../make-test-python.nix (
   { pkgs, lib, ... }: {
     name = "podman";
@@ -8,31 +6,31 @@ import ../make-test-python.nix (
     };
 
     nodes = {
-      podman =
-        { pkgs, ... }:
-        {
-          virtualisation.podman.enable = true;
-
-          # To test docker socket support
-          virtualisation.podman.dockerSocket.enable = true;
-          environment.systemPackages = [
-            pkgs.docker-client
-          ];
-
-          users.users.alice = {
-            isNormalUser = true;
-            home = "/home/alice";
-            description = "Alice Foobar";
-            extraGroups = [ "podman" ];
-          };
-
-          users.users.mallory = {
-            isNormalUser = true;
-            home = "/home/mallory";
-            description = "Mallory Foobar";
-          };
+      podman = { pkgs, ... }: {
+        virtualisation.podman.enable = true;
+
+        users.users.alice = {
+          isNormalUser = true;
+        };
+      };
+      docker = { pkgs, ... }: {
+        virtualisation.podman.enable = true;
+
+        virtualisation.podman.dockerSocket.enable = true;
+
+        environment.systemPackages = [
+          pkgs.docker-client
+        ];
 
+        users.users.alice = {
+          isNormalUser = true;
+          extraGroups = [ "podman" ];
         };
+
+        users.users.mallory = {
+          isNormalUser = true;
+        };
+      };
     };
 
     testScript = ''
@@ -45,6 +43,7 @@ import ../make-test-python.nix (
 
 
       podman.wait_for_unit("sockets.target")
+      docker.wait_for_unit("sockets.target")
       start_all()
 
       with subtest("Run container as root with runc"):
@@ -74,8 +73,10 @@ import ../make-test-python.nix (
           podman.succeed("podman stop sleeping")
           podman.succeed("podman rm sleeping")
 
-      # create systemd session for rootless
+      # start systemd session for rootless
       podman.succeed("loginctl enable-linger alice")
+      podman.succeed(su_cmd("whoami"))
+      podman.sleep(1)
 
       with subtest("Run container rootless with runc"):
           podman.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
@@ -120,22 +121,22 @@ import ../make-test-python.nix (
           assert pid == "2"
 
       with subtest("A podman member can use the docker cli"):
-          podman.succeed(su_cmd("docker version"))
+          docker.succeed(su_cmd("docker version"))
 
       with subtest("Run container via docker cli"):
-          podman.succeed("docker network create default")
-          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
-          podman.succeed(
+          docker.succeed("docker network create default")
+          docker.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
+          docker.succeed(
             "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin localhost/scratchimg /bin/sleep 10"
           )
-          podman.succeed("docker ps | grep sleeping")
-          podman.succeed("podman ps | grep sleeping")
-          podman.succeed("docker stop sleeping")
-          podman.succeed("docker rm sleeping")
-          podman.succeed("docker network rm default")
+          docker.succeed("docker ps | grep sleeping")
+          docker.succeed("podman ps | grep sleeping")
+          docker.succeed("docker stop sleeping")
+          docker.succeed("docker rm sleeping")
+          docker.succeed("docker network rm default")
 
       with subtest("A podman non-member can not use the docker cli"):
-          podman.fail(su_cmd("docker version", user="mallory"))
+          docker.fail(su_cmd("docker version", user="mallory"))
 
       # TODO: add docker-compose test
 
diff --git a/nixos/tests/printing.nix b/nixos/tests/printing.nix
index 6338fd8d8ac10..cfebe232d92a0 100644
--- a/nixos/tests/printing.nix
+++ b/nixos/tests/printing.nix
@@ -4,6 +4,7 @@ import ./make-test-python.nix ({pkgs, ... }:
 let
   printingServer = startWhenNeeded: {
     services.printing.enable = true;
+    services.printing.stateless = true;
     services.printing.startWhenNeeded = startWhenNeeded;
     services.printing.listenAddresses = [ "*:631" ];
     services.printing.defaultShared = true;
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
index 596a4eafcd642..d91fc52f1cb45 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -374,25 +374,34 @@ let
     };
 
     kea = let
-      controlSocketPath = "/run/kea/dhcp6.sock";
+      controlSocketPathV4 = "/run/kea/dhcp4.sock";
+      controlSocketPathV6 = "/run/kea/dhcp6.sock";
     in
     {
       exporterConfig = {
         enable = true;
         controlSocketPaths = [
-          controlSocketPath
+          controlSocketPathV4
+          controlSocketPathV6
         ];
       };
       metricProvider = {
-        systemd.services.prometheus-kea-exporter.after = [ "kea-dhcp6-server.service" ];
-
         services.kea = {
+          dhcp4 = {
+            enable = true;
+            settings = {
+              control-socket = {
+                socket-type = "unix";
+                socket-name = controlSocketPathV4;
+              };
+            };
+          };
           dhcp6 = {
             enable = true;
             settings = {
               control-socket = {
                 socket-type = "unix";
-                socket-name = controlSocketPath;
+                socket-name = controlSocketPathV6;
               };
             };
           };
@@ -400,8 +409,10 @@ let
       };
 
       exporterTest = ''
+        wait_for_unit("kea-dhcp4-server.service")
         wait_for_unit("kea-dhcp6-server.service")
-        wait_for_file("${controlSocketPath}")
+        wait_for_file("${controlSocketPathV4}")
+        wait_for_file("${controlSocketPathV6}")
         wait_for_unit("prometheus-kea-exporter.service")
         wait_for_open_port(9547)
         succeed(
@@ -1385,6 +1396,22 @@ let
           )
         '';
       };
+
+    zfs = {
+      exporterConfig = {
+        enable = true;
+      };
+      metricProvider = {
+        boot.supportedFilesystems = [ "zfs" ];
+        networking.hostId = "7327ded7";
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-zfs-exporter.service")
+        wait_for_unit("zfs.target")
+        wait_for_open_port(9134)
+        wait_until_succeeds("curl -f localhost:9134/metrics | grep 'zfs_scrape_collector_success{.*} 1'")
+      '';
+    };
   };
 in
 mapAttrs
diff --git a/nixos/tests/quake3.nix b/nixos/tests/quake3.nix
new file mode 100644
index 0000000000000..82af1af463d03
--- /dev/null
+++ b/nixos/tests/quake3.nix
@@ -0,0 +1,95 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+
+let
+
+  # Build Quake with coverage instrumentation.
+  overrides = pkgs:
+    {
+      quake3game = pkgs.quake3game.override (args: {
+        stdenv = pkgs.stdenvAdapters.addCoverageInstrumentation args.stdenv;
+      });
+    };
+
+  # Only allow the demo data to be used (only if it's unfreeRedistributable).
+  unfreePredicate = pkg: with pkgs.lib; let
+    allowPackageNames = [ "quake3-demodata" "quake3-pointrelease" ];
+    allowLicenses = [ pkgs.lib.licenses.unfreeRedistributable ];
+  in elem pkg.pname allowPackageNames &&
+     elem (pkg.meta.license or null) allowLicenses;
+
+  client =
+    { pkgs, ... }:
+
+    { imports = [ ./common/x11.nix ];
+      hardware.opengl.driSupport = true;
+      environment.systemPackages = [ pkgs.quake3demo ];
+      nixpkgs.config.packageOverrides = overrides;
+      nixpkgs.config.allowUnfreePredicate = unfreePredicate;
+    };
+
+in
+
+rec {
+  name = "quake3";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ domenkozar eelco ];
+  };
+
+  # TODO: lcov doesn't work atm
+  #makeCoverageReport = true;
+
+  nodes =
+    { server =
+        { pkgs, ... }:
+
+        { systemd.services.quake3-server =
+            { wantedBy = [ "multi-user.target" ];
+              script =
+                "${pkgs.quake3demo}/bin/quake3-server +set g_gametype 0 " +
+                "+map q3dm7 +addbot grunt +addbot daemia 2> /tmp/log";
+            };
+          nixpkgs.config.packageOverrides = overrides;
+          nixpkgs.config.allowUnfreePredicate = unfreePredicate;
+          networking.firewall.allowedUDPPorts = [ 27960 ];
+        };
+
+      client1 = client;
+      client2 = client;
+    };
+
+  testScript =
+    ''
+      start_all()
+
+      server.wait_for_unit("quake3-server")
+      client1.wait_for_x()
+      client2.wait_for_x()
+
+      client1.execute("quake3 +set r_fullscreen 0 +set name Foo +connect server &")
+      client2.execute("quake3 +set r_fullscreen 0 +set name Bar +connect server &")
+
+      server.wait_until_succeeds("grep -q 'Foo.*entered the game' /tmp/log")
+      server.wait_until_succeeds("grep -q 'Bar.*entered the game' /tmp/log")
+
+      server.sleep(10)  # wait for a while to get a nice screenshot
+
+      client1.block()
+
+      server.sleep(20)
+
+      client1.screenshot("screen1")
+      client2.screenshot("screen2")
+
+      client1.unblock()
+
+      server.sleep(10)
+
+      client1.screenshot("screen3")
+      client2.screenshot("screen4")
+
+      client1.shutdown()
+      client2.shutdown()
+      server.stop_job("quake3-server")
+    '';
+
+})
diff --git a/nixos/tests/shadow.nix b/nixos/tests/shadow.nix
index 50a9f71246469..baa2e5945c05d 100644
--- a/nixos/tests/shadow.nix
+++ b/nixos/tests/shadow.nix
@@ -3,6 +3,8 @@ let
   password2 = "helloworld";
   password3 = "bazqux";
   password4 = "asdf123";
+  hashed_bcrypt = "$2b$05$8xIEflrk2RxQtcVXbGIxs.Vl0x7dF1/JSv3cyX6JJt0npzkTCWvxK"; # fnord
+  hashed_yeshash = "$y$j9T$d8Z4EAf8P1SvM/aDFbxMS0$VnTXMp/Hnc7QdCBEaLTq5ZFOAFo2/PM0/xEAFuOE88."; # fnord
 in import ./make-test-python.nix ({ pkgs, ... }: {
   name = "shadow";
   meta = with pkgs.lib.maintainers; { maintainers = [ nequissimus ]; };
@@ -27,6 +29,16 @@ in import ./make-test-python.nix ({ pkgs, ... }: {
         password = password4;
         shell = pkgs.bash;
       };
+      users.berta = {
+        isNormalUser = true;
+        hashedPassword = hashed_bcrypt;
+        shell = pkgs.bash;
+      };
+      users.yesim = {
+        isNormalUser = true;
+        hashedPassword = hashed_yeshash;
+        shell = pkgs.bash;
+      };
     };
   };
 
@@ -115,5 +127,23 @@ in import ./make-test-python.nix ({ pkgs, ... }: {
         shadow.wait_until_succeeds("pgrep login")
         shadow.send_chars("${password2}\n")
         shadow.wait_until_tty_matches("5", "login:")
+
+    with subtest("check alternate password hashes"):
+        shadow.send_key("alt-f6")
+        shadow.wait_until_succeeds("[ $(fgconsole) = 6 ]")
+        for u in ["berta", "yesim"]:
+            shadow.wait_for_unit("getty@tty6.service")
+            shadow.wait_until_succeeds("pgrep -f 'agetty.*tty6'")
+            shadow.wait_until_tty_matches("6", "login: ")
+            shadow.send_chars(f"{u}\n")
+            shadow.wait_until_tty_matches("6", f"login: {u}")
+            shadow.wait_until_succeeds("pgrep login")
+            shadow.sleep(2)
+            shadow.send_chars("fnord\n")
+            shadow.send_chars(f"whoami > /tmp/{u}\n")
+            shadow.wait_for_file(f"/tmp/{u}")
+            print(shadow.succeed(f"cat /tmp/{u}"))
+            assert u in shadow.succeed(f"cat /tmp/{u}")
+            shadow.send_chars("logout\n")
   '';
 })
diff --git a/nixos/tests/smokeping.nix b/nixos/tests/smokeping.nix
index ccacf60cfe4bd..04f8139642918 100644
--- a/nixos/tests/smokeping.nix
+++ b/nixos/tests/smokeping.nix
@@ -28,6 +28,8 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     sm.wait_for_unit("thttpd")
     sm.wait_for_file("/var/lib/smokeping/data/Local/LocalMachine.rrd")
     sm.succeed("curl -s -f localhost:8081/smokeping.fcgi?target=Local")
+    # Check that there's a helpful page without explicit path as well.
+    sm.succeed("curl -s -f localhost:8081")
     sm.succeed("ls /var/lib/smokeping/cache/Local/LocalMachine_mini.png")
     sm.succeed("ls /var/lib/smokeping/cache/index.html")
   '';
diff --git a/nixos/tests/systemd-initrd-luks-fido2.nix b/nixos/tests/systemd-initrd-luks-fido2.nix
new file mode 100644
index 0000000000000..133e552a3dc99
--- /dev/null
+++ b/nixos/tests/systemd-initrd-luks-fido2.nix
@@ -0,0 +1,45 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "systemd-initrd-luks-fido2";
+
+  nodes.machine = { pkgs, config, ... }: {
+    # Use systemd-boot
+    virtualisation = {
+      emptyDiskImages = [ 512 ];
+      useBootLoader = true;
+      useEFIBoot = true;
+      qemu.package = lib.mkForce (pkgs.qemu_test.override { canokeySupport = true; });
+      qemu.options = [ "-device canokey,file=/tmp/canokey-file" ];
+    };
+    boot.loader.systemd-boot.enable = true;
+
+    boot.initrd.systemd.enable = true;
+
+    environment.systemPackages = with pkgs; [ cryptsetup ];
+
+    specialisation.boot-luks.configuration = {
+      boot.initrd.luks.devices = lib.mkVMOverride {
+        cryptroot = {
+          device = "/dev/vdc";
+          crypttabExtraOpts = [ "fido2-device=auto" ];
+        };
+      };
+      virtualisation.bootDevice = "/dev/mapper/cryptroot";
+    };
+  };
+
+  testScript = ''
+    # Create encrypted volume
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /dev/vdc -")
+    machine.succeed("PASSWORD=supersecret SYSTEMD_LOG_LEVEL=debug systemd-cryptenroll --fido2-device=auto /dev/vdc |& systemd-cat")
+
+    # Boot from the encrypted disk
+    machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks.conf")
+    machine.succeed("sync")
+    machine.crash()
+
+    # Boot and decrypt the disk
+    machine.wait_for_unit("multi-user.target")
+    assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount")
+  '';
+})
diff --git a/nixos/tests/systemd-initrd-luks-tpm2.nix b/nixos/tests/systemd-initrd-luks-tpm2.nix
new file mode 100644
index 0000000000000..085088d2ee25e
--- /dev/null
+++ b/nixos/tests/systemd-initrd-luks-tpm2.nix
@@ -0,0 +1,72 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "systemd-initrd-luks-tpm2";
+
+  nodes.machine = { pkgs, ... }: {
+    # Use systemd-boot
+    virtualisation = {
+      emptyDiskImages = [ 512 ];
+      useBootLoader = true;
+      useEFIBoot = true;
+      qemu.options = ["-chardev socket,id=chrtpm,path=/tmp/mytpm1/swtpm-sock -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis,tpmdev=tpm0"];
+    };
+    boot.loader.systemd-boot.enable = true;
+
+    boot.initrd.availableKernelModules = [ "tpm_tis" ];
+
+    environment.systemPackages = with pkgs; [ cryptsetup ];
+    boot.initrd.systemd = {
+      enable = true;
+    };
+
+    specialisation.boot-luks.configuration = {
+      boot.initrd.luks.devices = lib.mkVMOverride {
+        cryptroot = {
+          device = "/dev/vdc";
+          crypttabExtraOpts = [ "tpm2-device=auto" ];
+        };
+      };
+      virtualisation.bootDevice = "/dev/mapper/cryptroot";
+    };
+  };
+
+  testScript = ''
+    import subprocess
+    import os
+    import time
+
+
+    class Tpm:
+        def __init__(self):
+            os.mkdir("/tmp/mytpm1")
+            self.start()
+
+        def start(self):
+            self.proc = subprocess.Popen(["${pkgs.swtpm}/bin/swtpm", "socket", "--tpmstate", "dir=/tmp/mytpm1", "--ctrl", "type=unixio,path=/tmp/mytpm1/swtpm-sock", "--log", "level=20", "--tpm2"])
+
+        def wait_for_death_then_restart(self):
+            while self.proc.poll() is None:
+                print("waiting for tpm to die")
+                time.sleep(1)
+            assert self.proc.returncode == 0
+            self.start()
+
+    tpm = Tpm()
+
+
+    # Create encrypted volume
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /dev/vdc -")
+    machine.succeed("PASSWORD=supersecret SYSTEMD_LOG_LEVEL=debug systemd-cryptenroll --tpm2-pcrs= --tpm2-device=auto /dev/vdc |& systemd-cat")
+
+    # Boot from the encrypted disk
+    machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks.conf")
+    machine.succeed("sync")
+    machine.crash()
+
+    tpm.wait_for_death_then_restart()
+
+    # Boot and decrypt the disk
+    machine.wait_for_unit("multi-user.target")
+    assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount")
+  '';
+})
diff --git a/nixos/tests/systemd-machinectl.nix b/nixos/tests/systemd-machinectl.nix
index 7a4888ab85f6a..b8ed0c33e8e4a 100644
--- a/nixos/tests/systemd-machinectl.nix
+++ b/nixos/tests/systemd-machinectl.nix
@@ -41,6 +41,17 @@ import ./make-test-python.nix ({ pkgs, ... }:
       systemd.targets.machines.wants = [ "systemd-nspawn@${containerName}.service" ];
 
       virtualisation.additionalPaths = [ containerSystem ];
+
+      # not needed, but we want to test the nspawn file generation
+      systemd.nspawn.${containerName} = { };
+
+      systemd.services."systemd-nspawn@${containerName}" = {
+        serviceConfig.Environment = [
+          # Disable tmpfs for /tmp
+          "SYSTEMD_NSPAWN_TMPFS_TMP=0"
+        ];
+        overrideStrategy = "asDropin";
+      };
     };
 
     testScript = ''
@@ -92,6 +103,9 @@ import ./make-test-python.nix ({ pkgs, ... }:
       machine.succeed("machinectl stop ${containerName}");
       machine.wait_until_succeeds("test $(systemctl is-active systemd-nspawn@${containerName}) = inactive");
 
+      # Test tmpfs for /tmp
+      machine.fail("mountpoint /tmp");
+
       # Show to to delete the container
       machine.succeed("chattr -i ${containerRoot}/var/empty");
       machine.succeed("rm -rf ${containerRoot}");
diff --git a/nixos/tests/systemd-no-tainted.nix b/nixos/tests/systemd-no-tainted.nix
new file mode 100644
index 0000000000000..f0504065f2a48
--- /dev/null
+++ b/nixos/tests/systemd-no-tainted.nix
@@ -0,0 +1,14 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "systemd-no-tainted";
+
+  nodes.machine = { };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+    with subtest("systemctl should not report tainted with unmerged-usr"):
+        output = machine.succeed("systemctl status")
+        print(output)
+        assert "Tainted" not in output
+        assert "unmerged-usr" not in output
+  '';
+})
diff --git a/nixos/tests/systemd-portabled.nix b/nixos/tests/systemd-portabled.nix
new file mode 100644
index 0000000000000..ef38258b0d866
--- /dev/null
+++ b/nixos/tests/systemd-portabled.nix
@@ -0,0 +1,51 @@
+import ./make-test-python.nix ({pkgs, lib, ...}: let
+  demo-program = pkgs.writeShellScriptBin "demo" ''
+      while ${pkgs.coreutils}/bin/sleep 3; do
+          echo Hello World > /dev/null
+      done
+  '';
+  demo-service = pkgs.writeText "demo.service" ''
+    [Unit]
+    Description=demo service
+    Requires=demo.socket
+    After=demo.socket
+
+    [Service]
+    Type=simple
+    ExecStart=${demo-program}/bin/demo
+    Restart=always
+
+    [Install]
+    WantedBy=multi-user.target
+    Also=demo.socket
+  '';
+  demo-socket = pkgs.writeText "demo.socket" ''
+    [Unit]
+    Description=demo socket
+
+    [Socket]
+    ListenStream=/run/demo.sock
+    SocketMode=0666
+
+    [Install]
+    WantedBy=sockets.target
+  '';
+  demo-portable = pkgs.portableService {
+    pname = "demo";
+    version = "1.0";
+    description = ''A demo "Portable Service" for a shell program built with nix'';
+    units = [ demo-service demo-socket ];
+  };
+in {
+
+  name = "systemd-portabled";
+  nodes.machine = {};
+  testScript = ''
+    machine.succeed("portablectl")
+    machine.wait_for_unit("systemd-portabled.service")
+    machine.succeed("portablectl attach --now --runtime ${demo-portable}/demo_1.0.raw")
+    machine.wait_for_unit("demo.service")
+    machine.succeed("portablectl detach --now --runtime demo_1.0")
+    machine.fail("systemctl status demo.service")
+  '';
+})
diff --git a/nixos/tests/tracee.nix b/nixos/tests/tracee.nix
index 26d0ada931b1c..72e82ec0b7edf 100644
--- a/nixos/tests/tracee.nix
+++ b/nixos/tests/tracee.nix
@@ -14,15 +14,18 @@ import ./make-test-python.nix ({ pkgs, ... }: {
           patches = oa.patches or [] ++ [
             # change the prefix from /usr/bin to /run to find nix processes
             ../../pkgs/tools/security/tracee/test-EventFilters-prefix-nix-friendly.patch
-            # skip magic_write test that currently fails
-            ../../pkgs/tools/security/tracee/test-EventFilters-magic_write-skip.patch
           ];
           buildPhase = ''
             runHook preBuild
             # just build the static lib we need for the go test binary
-            make $makeFlags ''${enableParallelBuilding:+-j$NIX_BUILD_CORES -l$NIX_BUILD_CORES} bpf-core ./dist/btfhub ./dist/libbpf/libbpf.a
+            make $makeFlags ''${enableParallelBuilding:+-j$NIX_BUILD_CORES -l$NIX_BUILD_CORES} bpf-core ./dist/btfhub
+
+            # remove the /usr/bin prefix to work with the patch above
+            substituteInPlace tests/integration/integration_test.go \
+              --replace "/usr/bin/ls" "ls"
+
             # then compile the tests to be ran later
-            CGO_CFLAGS="-I$PWD/dist/libbpf" CGO_LDFLAGS="-lelf -lz $PWD/dist/libbpf/libbpf.a" go test -tags core,ebpf,integration -p 1 -c -o $GOPATH/tracee-integration ./tests/integration/...
+            CGO_LDFLAGS="$(pkg-config --libs libbpf)" go test -tags core,ebpf,integration -p 1 -c -o $GOPATH/tracee-integration ./tests/integration/...
             runHook postBuild
           '';
           doCheck = false;
diff --git a/nixos/tests/upnp.nix b/nixos/tests/upnp.nix
index 451c8607d0ebd..af7cc1fe24130 100644
--- a/nixos/tests/upnp.nix
+++ b/nixos/tests/upnp.nix
@@ -47,7 +47,7 @@ in
 
       client1 =
         { pkgs, nodes, ... }:
-        { environment.systemPackages = [ pkgs.miniupnpc_2 pkgs.netcat ];
+        { environment.systemPackages = [ pkgs.miniupnpc pkgs.netcat ];
           virtualisation.vlans = [ 2 ];
           networking.defaultGateway = internalRouterAddress;
           networking.interfaces.eth1.ipv4.addresses = [
@@ -65,7 +65,7 @@ in
 
       client2 =
         { pkgs, ... }:
-        { environment.systemPackages = [ pkgs.miniupnpc_2 ];
+        { environment.systemPackages = [ pkgs.miniupnpc ];
           virtualisation.vlans = [ 1 ];
           networking.interfaces.eth1.ipv4.addresses = [
             { address = externalClient2Address; prefixLength = 24; }
diff --git a/nixos/tests/uptime-kuma.nix b/nixos/tests/uptime-kuma.nix
new file mode 100644
index 0000000000000..3d588d73cdb5c
--- /dev/null
+++ b/nixos/tests/uptime-kuma.nix
@@ -0,0 +1,19 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "uptime-kuma";
+  meta.maintainers = with maintainers; [ julienmalka ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    { services.uptime-kuma.enable = true; };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("uptime-kuma.service")
+    machine.wait_for_open_port(3001)
+    machine.succeed("curl --fail http://localhost:3001/")
+  '';
+})
diff --git a/nixos/tests/web-apps/healthchecks.nix b/nixos/tests/web-apps/healthchecks.nix
index 41374f5e314bb..41c40cd5dd8d4 100644
--- a/nixos/tests/web-apps/healthchecks.nix
+++ b/nixos/tests/web-apps/healthchecks.nix
@@ -33,10 +33,10 @@ import ../make-test-python.nix ({ lib, pkgs, ... }: {
         )
 
     with subtest("Manage script works"):
-        # Should fail if not called by healthchecks user
-        machine.fail("echo 'print(\"foo\")' | healthchecks-manage help")
-
         # "shell" sucommand should succeed, needs python in PATH.
         assert "foo\n" == machine.succeed("echo 'print(\"foo\")' | sudo -u healthchecks healthchecks-manage shell")
+
+        # Shouldn't fail if not called by healthchecks user
+        assert "foo\n" == machine.succeed("echo 'print(\"foo\")' | healthchecks-manage shell")
   '';
 })
diff --git a/nixos/tests/web-apps/mastodon.nix b/nixos/tests/web-apps/mastodon.nix
index 279a1c59169fa..bc1122e7268f9 100644
--- a/nixos/tests/web-apps/mastodon.nix
+++ b/nixos/tests/web-apps/mastodon.nix
@@ -16,7 +16,7 @@ let
 in
 {
   name = "mastodon";
-  meta.maintainers = with pkgs.lib.maintainers; [ erictapen izorkin ];
+  meta.maintainers = with pkgs.lib.maintainers; [ erictapen izorkin turion ];
 
   nodes = {
     ca = { pkgs, ... }: {
@@ -56,6 +56,9 @@ in
     };
 
     server = { pkgs, ... }: {
+
+      virtualisation.memorySize = 2048;
+
       networking = {
         interfaces.eth1 = {
           ipv4.addresses = [
@@ -129,6 +132,9 @@ in
     ca.wait_for_unit("step-ca.service")
     ca.wait_for_open_port(8443)
 
+    # Check that mastodon-media-auto-remove is scheduled
+    server.succeed("systemctl status mastodon-media-auto-remove.timer")
+
     server.wait_for_unit("nginx.service")
     server.wait_for_unit("redis-mastodon.service")
     server.wait_for_unit("postgresql.service")
diff --git a/nixos/tests/wrappers.nix b/nixos/tests/wrappers.nix
new file mode 100644
index 0000000000000..08c1ad0b6b999
--- /dev/null
+++ b/nixos/tests/wrappers.nix
@@ -0,0 +1,79 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+  userUid = 1000;
+  usersGid = 100;
+  busybox = pkgs : pkgs.busybox.override {
+    # Without this, the busybox binary drops euid to ruid for most applets, including id.
+    # See https://bugs.busybox.net/show_bug.cgi?id=15101
+    extraConfig = "CONFIG_FEATURE_SUID n";
+  };
+in
+{
+  name = "wrappers";
+
+  nodes.machine = { config, pkgs, ... }: {
+    ids.gids.users = usersGid;
+
+    users.users = {
+      regular = {
+        uid = userUid;
+        isNormalUser = true;
+      };
+    };
+
+    security.wrappers = {
+      suidRoot = {
+        owner = "root";
+        group = "root";
+        setuid = true;
+        source = "${busybox pkgs}/bin/busybox";
+        program = "suid_root_busybox";
+      };
+      sgidRoot = {
+        owner = "root";
+        group = "root";
+        setgid = true;
+        source = "${busybox pkgs}/bin/busybox";
+        program = "sgid_root_busybox";
+      };
+      withChown = {
+        owner = "root";
+        group = "root";
+        source = "${pkgs.libcap}/bin/capsh";
+        program = "capsh_with_chown";
+        capabilities = "cap_chown+ep";
+      };
+    };
+  };
+
+  testScript =
+    ''
+      def cmd_as_regular(cmd):
+        return "su -l regular -c '{0}'".format(cmd)
+
+      def test_as_regular(cmd, expected):
+        out = machine.succeed(cmd_as_regular(cmd)).strip()
+        assert out == expected, "Expected {0} to output {1}, but got {2}".format(cmd, expected, out)
+
+      test_as_regular('${busybox pkgs}/bin/busybox id -u', '${toString userUid}')
+      test_as_regular('${busybox pkgs}/bin/busybox id -ru', '${toString userUid}')
+      test_as_regular('${busybox pkgs}/bin/busybox id -g', '${toString usersGid}')
+      test_as_regular('${busybox pkgs}/bin/busybox id -rg', '${toString usersGid}')
+
+      test_as_regular('/run/wrappers/bin/suid_root_busybox id -u', '0')
+      test_as_regular('/run/wrappers/bin/suid_root_busybox id -ru', '${toString userUid}')
+      test_as_regular('/run/wrappers/bin/suid_root_busybox id -g', '${toString usersGid}')
+      test_as_regular('/run/wrappers/bin/suid_root_busybox id -rg', '${toString usersGid}')
+
+      test_as_regular('/run/wrappers/bin/sgid_root_busybox id -u', '${toString userUid}')
+      test_as_regular('/run/wrappers/bin/sgid_root_busybox id -ru', '${toString userUid}')
+      test_as_regular('/run/wrappers/bin/sgid_root_busybox id -g', '0')
+      test_as_regular('/run/wrappers/bin/sgid_root_busybox id -rg', '${toString usersGid}')
+
+      # We are only testing the permitted set, because it's easiest to look at with capsh.
+      machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_CHOWN'))
+      machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_SYS_ADMIN'))
+      machine.succeed(cmd_as_regular('/run/wrappers/bin/capsh_with_chown --has-p=CAP_CHOWN'))
+      machine.fail(cmd_as_regular('/run/wrappers/bin/capsh_with_chown --has-p=CAP_SYS_ADMIN'))
+    '';
+})
diff --git a/nixos/tests/zrepl.nix b/nixos/tests/zrepl.nix
index 0ed73fea34b0d..b16c7eddc7aec 100644
--- a/nixos/tests/zrepl.nix
+++ b/nixos/tests/zrepl.nix
@@ -58,8 +58,8 @@ import ./make-test-python.nix (
           out = host.succeed("curl -f localhost:9811/metrics")
 
           assert (
-              "zrepl_version_daemon" in out
-          ), "zrepl version metric was not found in Prometheus output"
+              "zrepl_start_time" in out
+          ), "zrepl start time metric was not found in Prometheus output"
 
           assert (
               "zrepl_zfs_snapshot_duration_count{filesystem=\"test\"}" in out