summary refs log tree commit diff
path: root/nixos/modules
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules')
-rw-r--r--nixos/modules/config/console.nix25
-rw-r--r--nixos/modules/config/fonts/fontconfig.nix78
-rw-r--r--nixos/modules/config/fonts/fontdir.nix2
-rw-r--r--nixos/modules/config/fonts/fonts.nix38
-rw-r--r--nixos/modules/config/gnu.nix1
-rw-r--r--nixos/modules/config/i18n.nix1
-rw-r--r--nixos/modules/config/malloc.nix3
-rw-r--r--nixos/modules/config/mysql.nix2
-rw-r--r--nixos/modules/config/nix-channel.nix70
-rw-r--r--nixos/modules/config/nix-flakes.nix95
-rw-r--r--nixos/modules/config/nix-remote-build.nix226
-rw-r--r--nixos/modules/config/nix.nix379
-rw-r--r--nixos/modules/config/no-x-libs.nix27
-rw-r--r--nixos/modules/config/qt.nix (renamed from nixos/modules/config/qt5.nix)42
-rw-r--r--nixos/modules/config/resolvconf.nix4
-rw-r--r--nixos/modules/config/stevenblack.nix34
-rw-r--r--nixos/modules/config/swap.nix74
-rw-r--r--nixos/modules/config/sysctl.nix3
-rw-r--r--nixos/modules/config/update-users-groups.pl10
-rw-r--r--nixos/modules/config/users-groups.nix134
-rw-r--r--nixos/modules/config/xdg/portal.nix2
-rw-r--r--nixos/modules/config/zram.nix179
-rw-r--r--nixos/modules/hardware/all-firmware.nix3
-rw-r--r--nixos/modules/hardware/device-tree.nix8
-rw-r--r--nixos/modules/hardware/flipperzero.nix18
-rw-r--r--nixos/modules/hardware/i2c.nix14
-rw-r--r--nixos/modules/hardware/keyboard/qmk.nix16
-rw-r--r--nixos/modules/hardware/keyboard/teck.nix6
-rw-r--r--nixos/modules/hardware/keyboard/uhk.nix7
-rw-r--r--nixos/modules/hardware/keyboard/zsa.nix19
-rw-r--r--nixos/modules/hardware/nitrokey.nix2
-rw-r--r--nixos/modules/hardware/opengl.nix8
-rw-r--r--nixos/modules/hardware/opentabletdriver.nix2
-rw-r--r--nixos/modules/hardware/printers.nix19
-rw-r--r--nixos/modules/hardware/sensor/hddtemp.nix2
-rw-r--r--nixos/modules/hardware/video/displaylink.nix1
-rw-r--r--nixos/modules/hardware/video/hidpi.nix24
-rw-r--r--nixos/modules/hardware/video/nvidia.nix126
-rw-r--r--nixos/modules/hardware/video/webcam/ipu6.nix57
-rw-r--r--nixos/modules/i18n/input-method/default.md160
-rw-r--r--nixos/modules/i18n/input-method/default.nix7
-rw-r--r--nixos/modules/i18n/input-method/default.xml291
-rw-r--r--nixos/modules/i18n/input-method/fcitx.nix46
-rw-r--r--nixos/modules/i18n/input-method/fcitx5.nix38
-rw-r--r--nixos/modules/i18n/input-method/ibus.nix5
-rw-r--r--nixos/modules/i18n/input-method/kime.nix68
-rw-r--r--nixos/modules/installer/cd-dvd/channel.nix2
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix2
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix1
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-minimal.nix4
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix99
-rw-r--r--nixos/modules/installer/netboot/netboot-minimal.nix3
-rw-r--r--nixos/modules/installer/netboot/netboot.nix19
-rw-r--r--nixos/modules/installer/sd-card/sd-image-powerpc64le.nix49
-rw-r--r--nixos/modules/installer/tools/nix-fallback-paths.nix10
-rwxr-xr-x[-rw-r--r--]nixos/modules/installer/tools/nixos-enter.sh7
-rw-r--r--nixos/modules/installer/tools/nixos-generate-config.pl63
-rwxr-xr-x[-rw-r--r--]nixos/modules/installer/tools/nixos-install.sh15
-rw-r--r--nixos/modules/installer/tools/nixos-version.sh9
-rw-r--r--nixos/modules/installer/tools/tools.nix27
-rw-r--r--nixos/modules/misc/documentation.nix28
-rw-r--r--nixos/modules/misc/ids.nix10
-rw-r--r--nixos/modules/misc/man-db.nix12
-rw-r--r--nixos/modules/misc/meta.nix2
-rw-r--r--nixos/modules/misc/nixpkgs.nix33
-rw-r--r--nixos/modules/misc/nixpkgs/read-only.nix74
-rw-r--r--nixos/modules/misc/nixpkgs/test.nix59
-rw-r--r--nixos/modules/misc/version.nix56
-rw-r--r--nixos/modules/module-list.nix125
-rw-r--r--nixos/modules/profiles/base.nix17
-rw-r--r--nixos/modules/profiles/headless.nix4
-rw-r--r--nixos/modules/profiles/installation-device.nix13
-rw-r--r--nixos/modules/profiles/macos-builder.nix310
-rw-r--r--nixos/modules/programs/_1password-gui.nix2
-rw-r--r--nixos/modules/programs/_1password.nix2
-rw-r--r--nixos/modules/programs/atop.nix3
-rw-r--r--nixos/modules/programs/ccache.nix2
-rw-r--r--nixos/modules/programs/cfs-zen-tweaks.nix8
-rw-r--r--nixos/modules/programs/clash-verge.nix41
-rw-r--r--nixos/modules/programs/darling.nix21
-rw-r--r--nixos/modules/programs/digitalbitbox/default.md47
-rw-r--r--nixos/modules/programs/digitalbitbox/default.nix2
-rw-r--r--nixos/modules/programs/digitalbitbox/doc.xml74
-rw-r--r--nixos/modules/programs/firefox.nix4
-rw-r--r--nixos/modules/programs/fish.nix2
-rw-r--r--nixos/modules/programs/flashrom.nix3
-rw-r--r--nixos/modules/programs/fzf.nix19
-rw-r--r--nixos/modules/programs/gamescope.nix85
-rw-r--r--nixos/modules/programs/gnome-documents.nix54
-rw-r--r--nixos/modules/programs/gnupg.nix92
-rw-r--r--nixos/modules/programs/hyprland.nix81
-rw-r--r--nixos/modules/programs/iay.nix37
-rw-r--r--nixos/modules/programs/java.nix24
-rw-r--r--nixos/modules/programs/k3b.nix4
-rw-r--r--nixos/modules/programs/less.nix2
-rw-r--r--nixos/modules/programs/minipro.nix29
-rw-r--r--nixos/modules/programs/miriway.nix78
-rw-r--r--nixos/modules/programs/nano.nix13
-rw-r--r--nixos/modules/programs/neovim.nix52
-rw-r--r--nixos/modules/programs/nexttrace.nix25
-rw-r--r--nixos/modules/programs/nix-ld.nix18
-rw-r--r--nixos/modules/programs/plotinus.md17
-rw-r--r--nixos/modules/programs/plotinus.nix2
-rw-r--r--nixos/modules/programs/plotinus.xml30
-rw-r--r--nixos/modules/programs/proxychains.nix8
-rw-r--r--nixos/modules/programs/qdmr.nix25
-rw-r--r--nixos/modules/programs/regreet.nix75
-rw-r--r--nixos/modules/programs/shadow.nix306
-rw-r--r--nixos/modules/programs/sharing.nix19
-rw-r--r--nixos/modules/programs/singularity.nix102
-rw-r--r--nixos/modules/programs/skim.nix4
-rw-r--r--nixos/modules/programs/sniffnet.nix24
-rw-r--r--nixos/modules/programs/ssh.nix11
-rw-r--r--nixos/modules/programs/starship.nix31
-rw-r--r--nixos/modules/programs/steam.nix105
-rw-r--r--nixos/modules/programs/streamdeck-ui.nix2
-rw-r--r--nixos/modules/programs/tmux.nix21
-rw-r--r--nixos/modules/programs/trippy.nix24
-rw-r--r--nixos/modules/programs/tsm-client.nix6
-rw-r--r--nixos/modules/programs/turbovnc.nix2
-rw-r--r--nixos/modules/programs/wayland/river.nix59
-rw-r--r--nixos/modules/programs/wayland/sway.nix (renamed from nixos/modules/programs/sway.nix)77
-rw-r--r--nixos/modules/programs/wayland/waybar.nix (renamed from nixos/modules/programs/waybar.nix)9
-rw-r--r--nixos/modules/programs/wayland/wayland-session.nix23
-rw-r--r--nixos/modules/programs/wireshark.nix2
-rw-r--r--nixos/modules/programs/xastir.nix2
-rw-r--r--nixos/modules/programs/xonsh.nix3
-rw-r--r--nixos/modules/programs/zsh/oh-my-zsh.md109
-rw-r--r--nixos/modules/programs/zsh/oh-my-zsh.nix2
-rw-r--r--nixos/modules/programs/zsh/oh-my-zsh.xml155
-rw-r--r--nixos/modules/programs/zsh/zsh-syntax-highlighting.nix1
-rw-r--r--nixos/modules/programs/zsh/zsh.nix3
-rw-r--r--nixos/modules/rename.nix18
-rw-r--r--nixos/modules/security/acme/default.md354
-rw-r--r--nixos/modules/security/acme/default.nix14
-rw-r--r--nixos/modules/security/acme/doc.xml414
-rw-r--r--nixos/modules/security/apparmor/includes.nix2
-rw-r--r--nixos/modules/security/audit.nix2
-rw-r--r--nixos/modules/security/ca.nix6
-rw-r--r--nixos/modules/security/doas.nix20
-rw-r--r--nixos/modules/security/ipa.nix258
-rw-r--r--nixos/modules/security/lock-kernel-modules.nix11
-rw-r--r--nixos/modules/security/pam.nix141
-rw-r--r--nixos/modules/security/pam_mount.nix18
-rw-r--r--nixos/modules/security/polkit.nix2
-rw-r--r--nixos/modules/security/sudo.nix4
-rw-r--r--nixos/modules/security/systemd-confinement.nix1
-rw-r--r--nixos/modules/security/tpm2.nix2
-rw-r--r--nixos/modules/security/wrappers/default.nix2
-rw-r--r--nixos/modules/services/amqp/activemq/default.nix25
-rw-r--r--nixos/modules/services/audio/gmediarender.nix116
-rw-r--r--nixos/modules/services/audio/gonic.nix89
-rw-r--r--nixos/modules/services/audio/hqplayerd.nix3
-rw-r--r--nixos/modules/services/audio/mpd.nix2
-rw-r--r--nixos/modules/services/audio/navidrome.nix4
-rw-r--r--nixos/modules/services/audio/roon-bridge.nix7
-rw-r--r--nixos/modules/services/audio/roon-server.nix5
-rw-r--r--nixos/modules/services/audio/snapserver.nix4
-rw-r--r--nixos/modules/services/audio/tts.nix151
-rw-r--r--nixos/modules/services/audio/wyoming/faster-whisper.nix186
-rw-r--r--nixos/modules/services/audio/wyoming/piper.nix174
-rw-r--r--nixos/modules/services/audio/ympd.nix40
-rw-r--r--nixos/modules/services/backup/automysqlbackup.nix21
-rw-r--r--nixos/modules/services/backup/borgbackup.md163
-rw-r--r--nixos/modules/services/backup/borgbackup.nix14
-rw-r--r--nixos/modules/services/backup/borgbackup.xml209
-rw-r--r--nixos/modules/services/backup/borgmatic.nix87
-rw-r--r--nixos/modules/services/backup/btrbk.nix57
-rw-r--r--nixos/modules/services/backup/mysql-backup.nix2
-rw-r--r--nixos/modules/services/backup/restic.nix35
-rw-r--r--nixos/modules/services/backup/sanoid.nix4
-rw-r--r--nixos/modules/services/backup/syncoid.nix4
-rw-r--r--nixos/modules/services/backup/zfs-replication.nix2
-rw-r--r--nixos/modules/services/blockchain/ethereum/geth.nix4
-rw-r--r--nixos/modules/services/cluster/hadoop/hbase.nix228
-rw-r--r--nixos/modules/services/cluster/k3s/default.nix13
-rw-r--r--nixos/modules/services/cluster/kubernetes/addon-manager.nix2
-rw-r--r--nixos/modules/services/cluster/kubernetes/addons/dns.nix11
-rw-r--r--nixos/modules/services/cluster/kubernetes/flannel.nix2
-rw-r--r--nixos/modules/services/cluster/kubernetes/kubelet.nix12
-rw-r--r--nixos/modules/services/cluster/kubernetes/pki.nix2
-rw-r--r--nixos/modules/services/computing/boinc/client.nix2
-rw-r--r--nixos/modules/services/computing/slurm/slurm.nix2
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/master.nix11
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/worker.nix7
-rw-r--r--nixos/modules/services/continuous-integration/buildkite-agents.nix2
-rw-r--r--nixos/modules/services/continuous-integration/gitea-actions-runner.nix237
-rw-r--r--nixos/modules/services/continuous-integration/github-runner.nix2
-rw-r--r--nixos/modules/services/continuous-integration/github-runner/options.nix56
-rw-r--r--nixos/modules/services/continuous-integration/github-runner/service.nix398
-rw-r--r--nixos/modules/services/continuous-integration/github-runners.nix2
-rw-r--r--nixos/modules/services/continuous-integration/gitlab-runner.nix104
-rw-r--r--nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix203
-rw-r--r--nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix9
-rw-r--r--nixos/modules/services/continuous-integration/hercules-ci-agent/settings.nix153
-rw-r--r--nixos/modules/services/continuous-integration/hydra/default.nix2
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/job-builder.nix29
-rw-r--r--nixos/modules/services/continuous-integration/woodpecker/agents.nix144
-rw-r--r--nixos/modules/services/continuous-integration/woodpecker/server.nix98
-rw-r--r--nixos/modules/services/databases/clickhouse.nix9
-rw-r--r--nixos/modules/services/databases/cockroachdb.nix2
-rw-r--r--nixos/modules/services/databases/couchdb.nix2
-rw-r--r--nixos/modules/services/databases/dgraph.nix4
-rw-r--r--nixos/modules/services/databases/firebird.nix2
-rw-r--r--nixos/modules/services/databases/foundationdb.md309
-rw-r--r--nixos/modules/services/databases/foundationdb.nix2
-rw-r--r--nixos/modules/services/databases/foundationdb.xml443
-rw-r--r--nixos/modules/services/databases/lldap.nix121
-rw-r--r--nixos/modules/services/databases/mongodb.nix2
-rw-r--r--nixos/modules/services/databases/neo4j.nix2
-rw-r--r--nixos/modules/services/databases/postgresql.md210
-rw-r--r--nixos/modules/services/databases/postgresql.nix31
-rw-r--r--nixos/modules/services/databases/postgresql.xml231
-rw-r--r--nixos/modules/services/desktops/deepin/app-services.nix36
-rw-r--r--nixos/modules/services/desktops/deepin/dde-api.nix50
-rw-r--r--nixos/modules/services/desktops/deepin/dde-daemon.nix40
-rw-r--r--nixos/modules/services/desktops/flatpak.md39
-rw-r--r--nixos/modules/services/desktops/flatpak.nix2
-rw-r--r--nixos/modules/services/desktops/flatpak.xml56
-rw-r--r--nixos/modules/services/desktops/gnome/evolution-data-server.nix4
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json39
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/client.conf.json31
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json28
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/jack.conf.json63
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json120
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json38
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json106
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json105
-rw-r--r--nixos/modules/services/desktops/pipewire/media-session/alsa-monitor.conf.json34
-rw-r--r--nixos/modules/services/desktops/pipewire/media-session/bluez-monitor.conf.json36
-rw-r--r--nixos/modules/services/desktops/pipewire/media-session/media-session.conf.json68
-rw-r--r--nixos/modules/services/desktops/pipewire/media-session/v4l2-monitor.conf.json30
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire-media-session.nix141
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire.nix111
-rw-r--r--nixos/modules/services/desktops/pipewire/wireplumber.nix4
-rw-r--r--nixos/modules/services/desktops/system76-scheduler.nix296
-rw-r--r--nixos/modules/services/development/blackfire.md39
-rw-r--r--nixos/modules/services/development/blackfire.nix2
-rw-r--r--nixos/modules/services/development/blackfire.xml46
-rw-r--r--nixos/modules/services/development/gemstash.nix103
-rw-r--r--nixos/modules/services/development/lorri.nix2
-rw-r--r--nixos/modules/services/development/zammad.nix4
-rw-r--r--nixos/modules/services/editors/emacs.md420
-rw-r--r--nixos/modules/services/editors/emacs.nix2
-rw-r--r--nixos/modules/services/editors/emacs.xml580
-rw-r--r--nixos/modules/services/games/asf.nix2
-rw-r--r--nixos/modules/services/games/factorio.nix2
-rw-r--r--nixos/modules/services/games/freeciv.nix4
-rw-r--r--nixos/modules/services/games/mchprs.nix341
-rw-r--r--nixos/modules/services/games/minetest-server.nix71
-rw-r--r--nixos/modules/services/hardware/asusd.nix64
-rw-r--r--nixos/modules/services/hardware/auto-cpufreq.nix21
-rw-r--r--nixos/modules/services/hardware/bluetooth.nix27
-rw-r--r--nixos/modules/services/hardware/fancontrol.nix7
-rw-r--r--nixos/modules/services/hardware/fwupd.nix43
-rw-r--r--nixos/modules/services/hardware/kanata.nix90
-rw-r--r--nixos/modules/services/hardware/keyd.nix126
-rw-r--r--nixos/modules/services/hardware/openrgb.nix5
-rw-r--r--nixos/modules/services/hardware/pcscd.nix3
-rw-r--r--nixos/modules/services/hardware/supergfxd.nix1
-rw-r--r--nixos/modules/services/hardware/throttled.nix8
-rw-r--r--nixos/modules/services/hardware/trezord.md17
-rw-r--r--nixos/modules/services/hardware/trezord.nix2
-rw-r--r--nixos/modules/services/hardware/trezord.xml26
-rw-r--r--nixos/modules/services/hardware/udev.nix17
-rw-r--r--nixos/modules/services/hardware/udisks2.nix24
-rw-r--r--nixos/modules/services/hardware/undervolt.nix4
-rw-r--r--nixos/modules/services/home-automation/esphome.nix136
-rw-r--r--nixos/modules/services/home-automation/evcc.nix2
-rw-r--r--nixos/modules/services/home-automation/home-assistant.nix14
-rw-r--r--nixos/modules/services/logging/graylog.nix4
-rw-r--r--nixos/modules/services/logging/logrotate.nix7
-rw-r--r--nixos/modules/services/logging/syslogd.nix2
-rw-r--r--nixos/modules/services/logging/vector.nix15
-rw-r--r--nixos/modules/services/mail/davmail.nix27
-rw-r--r--nixos/modules/services/mail/dovecot.nix14
-rw-r--r--nixos/modules/services/mail/exim.nix4
-rw-r--r--nixos/modules/services/mail/goeland.nix74
-rw-r--r--nixos/modules/services/mail/listmonk.nix2
-rw-r--r--nixos/modules/services/mail/maddy.nix210
-rw-r--r--nixos/modules/services/mail/mailman.md82
-rw-r--r--nixos/modules/services/mail/mailman.nix35
-rw-r--r--nixos/modules/services/mail/mailman.xml94
-rw-r--r--nixos/modules/services/mail/postfix.nix4
-rw-r--r--nixos/modules/services/mail/public-inbox.nix35
-rw-r--r--nixos/modules/services/mail/roundcube.nix26
-rw-r--r--nixos/modules/services/mail/rspamd.nix2
-rw-r--r--nixos/modules/services/mail/spamassassin.nix2
-rw-r--r--nixos/modules/services/mail/zeyple.nix125
-rw-r--r--nixos/modules/services/matrix/appservice-discord.nix16
-rw-r--r--nixos/modules/services/matrix/appservice-irc.nix2
-rw-r--r--nixos/modules/services/matrix/dendrite.nix15
-rw-r--r--nixos/modules/services/matrix/mautrix-facebook.nix5
-rw-r--r--nixos/modules/services/matrix/mautrix-telegram.nix5
-rw-r--r--nixos/modules/services/matrix/mjolnir.md110
-rw-r--r--nixos/modules/services/matrix/mjolnir.nix6
-rw-r--r--nixos/modules/services/matrix/mjolnir.xml134
-rw-r--r--nixos/modules/services/matrix/mx-puppet-discord.nix (renamed from nixos/modules/services/misc/mx-puppet-discord.nix)0
-rw-r--r--nixos/modules/services/matrix/synapse.md213
-rw-r--r--nixos/modules/services/matrix/synapse.nix27
-rw-r--r--nixos/modules/services/matrix/synapse.xml276
-rw-r--r--nixos/modules/services/misc/ankisyncd.nix22
-rw-r--r--nixos/modules/services/misc/atuin.nix21
-rw-r--r--nixos/modules/services/misc/autorandr.nix14
-rw-r--r--nixos/modules/services/misc/autosuspend.nix230
-rw-r--r--nixos/modules/services/misc/calibre-server.nix94
-rw-r--r--nixos/modules/services/misc/disnix.nix4
-rw-r--r--nixos/modules/services/misc/docker-registry.nix22
-rw-r--r--nixos/modules/services/misc/etcd.nix9
-rw-r--r--nixos/modules/services/misc/etebase-server.nix8
-rw-r--r--nixos/modules/services/misc/exhibitor.nix417
-rw-r--r--nixos/modules/services/misc/fstrim.nix2
-rw-r--r--nixos/modules/services/misc/gammu-smsd.nix10
-rw-r--r--nixos/modules/services/misc/gitea.nix267
-rw-r--r--nixos/modules/services/misc/gitit.nix725
-rw-r--r--nixos/modules/services/misc/gitlab.md112
-rw-r--r--nixos/modules/services/misc/gitlab.nix268
-rw-r--r--nixos/modules/services/misc/gitlab.xml151
-rw-r--r--nixos/modules/services/misc/gollum.nix24
-rw-r--r--nixos/modules/services/misc/gpsd.nix48
-rw-r--r--nixos/modules/services/misc/heisenbridge.nix2
-rw-r--r--nixos/modules/services/misc/input-remapper.nix4
-rw-r--r--nixos/modules/services/misc/jellyseerr.nix62
-rw-r--r--nixos/modules/services/misc/klipper.nix18
-rw-r--r--nixos/modules/services/misc/mbpfan.nix41
-rw-r--r--nixos/modules/services/misc/moonraker.nix51
-rw-r--r--nixos/modules/services/misc/n8n.nix17
-rw-r--r--nixos/modules/services/misc/nitter.nix32
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix837
-rw-r--r--nixos/modules/services/misc/nix-optimise.nix31
-rw-r--r--nixos/modules/services/misc/ntfy-sh.nix37
-rw-r--r--nixos/modules/services/misc/octoprint.nix3
-rw-r--r--nixos/modules/services/misc/paperless.nix72
-rw-r--r--nixos/modules/services/misc/polaris.nix2
-rw-r--r--nixos/modules/services/misc/portunus.nix7
-rw-r--r--nixos/modules/services/misc/pufferpanel.nix176
-rw-r--r--nixos/modules/services/misc/pykms.nix2
-rw-r--r--nixos/modules/services/misc/readarr.nix88
-rw-r--r--nixos/modules/services/misc/redmine.nix16
-rw-r--r--nixos/modules/services/misc/rshim.nix99
-rw-r--r--nixos/modules/services/misc/siproxd.nix8
-rw-r--r--nixos/modules/services/misc/snapper.nix148
-rw-r--r--nixos/modules/services/misc/sourcehut/default.md93
-rw-r--r--nixos/modules/services/misc/sourcehut/default.nix4
-rw-r--r--nixos/modules/services/misc/sourcehut/sourcehut.xml119
-rw-r--r--nixos/modules/services/misc/sssd.nix5
-rw-r--r--nixos/modules/services/misc/tandoor-recipes.nix1
-rw-r--r--nixos/modules/services/misc/taskserver/default.md93
-rw-r--r--nixos/modules/services/misc/taskserver/default.nix2
-rw-r--r--nixos/modules/services/misc/taskserver/doc.xml135
-rw-r--r--nixos/modules/services/misc/weechat.md46
-rw-r--r--nixos/modules/services/misc/weechat.nix4
-rw-r--r--nixos/modules/services/misc/weechat.xml66
-rw-r--r--nixos/modules/services/misc/zoneminder.nix14
-rw-r--r--nixos/modules/services/monitoring/apcupsd.nix17
-rw-r--r--nixos/modules/services/monitoring/below.nix106
-rw-r--r--nixos/modules/services/monitoring/cadvisor.nix2
-rw-r--r--nixos/modules/services/monitoring/cockpit.nix231
-rw-r--r--nixos/modules/services/monitoring/datadog-agent.nix2
-rw-r--r--nixos/modules/services/monitoring/grafana-agent.nix26
-rw-r--r--nixos/modules/services/monitoring/grafana-image-renderer.nix4
-rw-r--r--nixos/modules/services/monitoring/grafana.nix831
-rw-r--r--nixos/modules/services/monitoring/loki.nix6
-rw-r--r--nixos/modules/services/monitoring/mackerel-agent.nix2
-rw-r--r--nixos/modules/services/monitoring/mimir.nix9
-rw-r--r--nixos/modules/services/monitoring/netdata.nix45
-rw-r--r--nixos/modules/services/monitoring/opentelemetry-collector.nix73
-rw-r--r--nixos/modules/services/monitoring/parsedmarc.md4
-rw-r--r--nixos/modules/services/monitoring/parsedmarc.nix8
-rw-r--r--nixos/modules/services/monitoring/parsedmarc.xml124
-rw-r--r--nixos/modules/services/monitoring/prometheus/alertmanager-irc-relay.nix107
-rw-r--r--nixos/modules/services/monitoring/prometheus/alertmanager.nix24
-rw-r--r--nixos/modules/services/monitoring/prometheus/default.nix6
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.md180
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix22
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.xml248
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/collectd.nix4
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/graphite.nix41
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/junos-czerwonk.nix72
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/php-fpm.nix65
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/pihole.nix36
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/scaphandre.nix33
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/shelly.nix27
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix12
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix6
-rw-r--r--nixos/modules/services/monitoring/tuptime.nix6
-rw-r--r--nixos/modules/services/monitoring/unpoller.nix4
-rw-r--r--nixos/modules/services/monitoring/uptime-kuma.nix12
-rw-r--r--nixos/modules/services/monitoring/vmalert.nix136
-rw-r--r--nixos/modules/services/network-filesystems/ceph.nix31
-rw-r--r--nixos/modules/services/network-filesystems/glusterfs.nix4
-rw-r--r--nixos/modules/services/network-filesystems/kubo.nix138
-rw-r--r--nixos/modules/services/network-filesystems/litestream/default.md (renamed from nixos/modules/services/network-filesystems/litestream/litestream.xml)33
-rw-r--r--nixos/modules/services/network-filesystems/litestream/default.nix3
-rw-r--r--nixos/modules/services/network-filesystems/moosefs.nix6
-rw-r--r--nixos/modules/services/network-filesystems/openafs/lib.nix4
-rw-r--r--nixos/modules/services/network-filesystems/openafs/server.nix86
-rw-r--r--nixos/modules/services/network-filesystems/webdav-server-rs.nix8
-rw-r--r--nixos/modules/services/networking/acme-dns.nix154
-rw-r--r--nixos/modules/services/networking/adguardhome.nix16
-rw-r--r--nixos/modules/services/networking/alice-lg.nix101
-rw-r--r--nixos/modules/services/networking/avahi-daemon.nix58
-rw-r--r--nixos/modules/services/networking/bind.nix19
-rw-r--r--nixos/modules/services/networking/bird-lg.nix112
-rw-r--r--nixos/modules/services/networking/birdwatcher.nix129
-rw-r--r--nixos/modules/services/networking/blockbook-frontend.nix2
-rw-r--r--nixos/modules/services/networking/blocky.nix1
-rw-r--r--nixos/modules/services/networking/cgit.nix203
-rw-r--r--nixos/modules/services/networking/consul.nix8
-rw-r--r--nixos/modules/services/networking/ddclient.nix242
-rw-r--r--nixos/modules/services/networking/dhcpcd.nix22
-rw-r--r--nixos/modules/services/networking/dhcpd.nix7
-rw-r--r--nixos/modules/services/networking/envoy.nix51
-rw-r--r--nixos/modules/services/networking/fakeroute.nix22
-rw-r--r--nixos/modules/services/networking/firefox-syncserver.nix8
-rw-r--r--nixos/modules/services/networking/firefox-syncserver.xml77
-rw-r--r--nixos/modules/services/networking/firewall-nftables.nix16
-rw-r--r--nixos/modules/services/networking/firewall.nix4
-rw-r--r--nixos/modules/services/networking/gnunet.nix6
-rw-r--r--nixos/modules/services/networking/go-neb.nix3
-rw-r--r--nixos/modules/services/networking/harmonia.nix88
-rw-r--r--nixos/modules/services/networking/headscale.nix60
-rw-r--r--nixos/modules/services/networking/hostapd.nix1412
-rw-r--r--nixos/modules/services/networking/i2pd.nix53
-rw-r--r--nixos/modules/services/networking/imaginary.nix113
-rw-r--r--nixos/modules/services/networking/ircd-hybrid/builder.sh1
-rw-r--r--nixos/modules/services/networking/iscsi/root-initiator.nix4
-rw-r--r--nixos/modules/services/networking/ivpn.nix51
-rw-r--r--nixos/modules/services/networking/jicofo.nix48
-rw-r--r--nixos/modules/services/networking/jitsi-videobridge.nix19
-rw-r--r--nixos/modules/services/networking/kresd.nix6
-rw-r--r--nixos/modules/services/networking/legit.nix182
-rw-r--r--nixos/modules/services/networking/minidlna.nix38
-rw-r--r--nixos/modules/services/networking/mosquitto.nix4
-rw-r--r--nixos/modules/services/networking/mosquitto.xml147
-rw-r--r--nixos/modules/services/networking/multipath.nix9
-rw-r--r--nixos/modules/services/networking/murmur.nix32
-rw-r--r--nixos/modules/services/networking/nat.nix2
-rw-r--r--nixos/modules/services/networking/ndppd.nix2
-rw-r--r--nixos/modules/services/networking/nebula.nix71
-rw-r--r--nixos/modules/services/networking/netbird.nix5
-rw-r--r--nixos/modules/services/networking/networkd-dispatcher.nix98
-rw-r--r--nixos/modules/services/networking/nftables.nix51
-rw-r--r--nixos/modules/services/networking/nomad.nix26
-rw-r--r--nixos/modules/services/networking/ntopng.nix2
-rw-r--r--nixos/modules/services/networking/ntp/chrony.nix4
-rw-r--r--nixos/modules/services/networking/openconnect.nix3
-rw-r--r--nixos/modules/services/networking/openvpn.nix32
-rw-r--r--nixos/modules/services/networking/pdns-recursor.nix2
-rw-r--r--nixos/modules/services/networking/peroxide.nix131
-rw-r--r--nixos/modules/services/networking/picosnitch.nix26
-rw-r--r--nixos/modules/services/networking/pleroma.md180
-rw-r--r--nixos/modules/services/networking/pleroma.nix2
-rw-r--r--nixos/modules/services/networking/pleroma.xml188
-rw-r--r--nixos/modules/services/networking/powerdns.nix2
-rw-r--r--nixos/modules/services/networking/prosody.md72
-rw-r--r--nixos/modules/services/networking/prosody.nix8
-rw-r--r--nixos/modules/services/networking/prosody.xml87
-rw-r--r--nixos/modules/services/networking/radicale.nix8
-rw-r--r--nixos/modules/services/networking/redsocks.nix3
-rw-r--r--nixos/modules/services/networking/rpcbind.nix2
-rw-r--r--nixos/modules/services/networking/searx.nix59
-rw-r--r--nixos/modules/services/networking/shellhub-agent.nix2
-rw-r--r--nixos/modules/services/networking/sing-box.nix67
-rw-r--r--nixos/modules/services/networking/sitespeed-io.nix122
-rw-r--r--nixos/modules/services/networking/smokeping.nix86
-rw-r--r--nixos/modules/services/networking/soju.nix2
-rw-r--r--nixos/modules/services/networking/ssh/lshd.nix6
-rw-r--r--nixos/modules/services/networking/ssh/sshd.nix329
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix51
-rw-r--r--nixos/modules/services/networking/strongswan.nix6
-rw-r--r--nixos/modules/services/networking/stunnel.nix4
-rw-r--r--nixos/modules/services/networking/syncplay.nix44
-rw-r--r--nixos/modules/services/networking/syncthing.nix29
-rw-r--r--nixos/modules/services/networking/tailscale.nix7
-rw-r--r--nixos/modules/services/networking/teleport.nix12
-rw-r--r--nixos/modules/services/networking/thelounge.nix24
-rw-r--r--nixos/modules/services/networking/tmate-ssh-server.nix2
-rw-r--r--nixos/modules/services/networking/twingate.nix30
-rw-r--r--nixos/modules/services/networking/unbound.nix2
-rw-r--r--nixos/modules/services/networking/unifi.nix10
-rw-r--r--nixos/modules/services/networking/v2raya.nix49
-rw-r--r--nixos/modules/services/networking/vdirsyncer.nix2
-rw-r--r--nixos/modules/services/networking/vsftpd.nix2
-rw-r--r--nixos/modules/services/networking/webhook.nix2
-rw-r--r--nixos/modules/services/networking/wgautomesh.nix163
-rw-r--r--nixos/modules/services/networking/wireguard.nix33
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix14
-rw-r--r--nixos/modules/services/networking/wstunnel.nix429
-rw-r--r--nixos/modules/services/networking/xinetd.nix2
-rw-r--r--nixos/modules/services/networking/xray.nix3
-rw-r--r--nixos/modules/services/networking/yggdrasil.md (renamed from nixos/modules/services/networking/yggdrasil.xml)65
-rw-r--r--nixos/modules/services/networking/yggdrasil.nix202
-rw-r--r--nixos/modules/services/networking/zerobin.nix5
-rw-r--r--nixos/modules/services/networking/znc/default.nix2
-rw-r--r--nixos/modules/services/printing/cupsd.nix8
-rw-r--r--nixos/modules/services/search/kibana.nix4
-rw-r--r--nixos/modules/services/search/meilisearch.md14
-rw-r--r--nixos/modules/services/search/meilisearch.nix6
-rw-r--r--nixos/modules/services/search/meilisearch.xml85
-rw-r--r--nixos/modules/services/search/opensearch.nix248
-rw-r--r--nixos/modules/services/search/qdrant.nix129
-rw-r--r--nixos/modules/services/search/solr.nix110
-rw-r--r--nixos/modules/services/security/authelia.nix401
-rw-r--r--nixos/modules/services/security/fail2ban.nix299
-rw-r--r--nixos/modules/services/security/kanidm.nix105
-rw-r--r--nixos/modules/services/security/oauth2_proxy.nix5
-rw-r--r--nixos/modules/services/security/privacyidea.nix4
-rw-r--r--nixos/modules/services/security/tor.nix5
-rw-r--r--nixos/modules/services/security/usbguard.nix125
-rw-r--r--nixos/modules/services/security/vault-agent.nix128
-rw-r--r--nixos/modules/services/security/vault.nix1
-rw-r--r--nixos/modules/services/security/vaultwarden/default.nix7
-rw-r--r--nixos/modules/services/security/yubikey-agent.nix3
-rw-r--r--nixos/modules/services/system/cachix-agent/default.nix2
-rw-r--r--nixos/modules/services/system/cachix-watch-store.nix8
-rw-r--r--nixos/modules/services/system/cloud-init.nix292
-rw-r--r--nixos/modules/services/system/dbus.nix21
-rw-r--r--nixos/modules/services/system/nix-daemon.nix262
-rw-r--r--nixos/modules/services/system/nscd.nix10
-rw-r--r--nixos/modules/services/system/self-deploy.nix8
-rw-r--r--nixos/modules/services/torrent/deluge.nix2
-rw-r--r--nixos/modules/services/torrent/flexget.nix4
-rw-r--r--nixos/modules/services/torrent/magnetico.nix2
-rw-r--r--nixos/modules/services/torrent/rtorrent.nix11
-rw-r--r--nixos/modules/services/torrent/transmission.nix6
-rw-r--r--nixos/modules/services/ttys/kmscon.nix1
-rw-r--r--nixos/modules/services/video/epgstation/default.nix28
-rw-r--r--nixos/modules/services/video/frigate.nix368
-rw-r--r--nixos/modules/services/video/go2rtc/default.nix115
-rw-r--r--nixos/modules/services/video/mediamtx.nix (renamed from nixos/modules/services/video/rtsp-simple-server.nix)32
-rw-r--r--nixos/modules/services/video/mirakurun.nix6
-rw-r--r--nixos/modules/services/video/unifi-video.nix2
-rw-r--r--nixos/modules/services/video/v4l2-relayd.nix199
-rw-r--r--nixos/modules/services/web-apps/akkoma.md12
-rw-r--r--nixos/modules/services/web-apps/akkoma.nix14
-rw-r--r--nixos/modules/services/web-apps/akkoma.xml396
-rw-r--r--nixos/modules/services/web-apps/alps.nix2
-rw-r--r--nixos/modules/services/web-apps/anuko-time-tracker.nix388
-rw-r--r--nixos/modules/services/web-apps/baget.nix170
-rw-r--r--nixos/modules/services/web-apps/changedetection-io.nix2
-rw-r--r--nixos/modules/services/web-apps/chatgpt-retrieval-plugin.nix106
-rw-r--r--nixos/modules/services/web-apps/cloudlog.nix503
-rw-r--r--nixos/modules/services/web-apps/code-server.nix230
-rw-r--r--nixos/modules/services/web-apps/coder.nix217
-rw-r--r--nixos/modules/services/web-apps/dex.nix2
-rw-r--r--nixos/modules/services/web-apps/discourse.md286
-rw-r--r--nixos/modules/services/web-apps/discourse.nix14
-rw-r--r--nixos/modules/services/web-apps/discourse.xml355
-rw-r--r--nixos/modules/services/web-apps/dokuwiki.nix402
-rw-r--r--nixos/modules/services/web-apps/dolibarr.nix4
-rw-r--r--nixos/modules/services/web-apps/freshrss.nix49
-rw-r--r--nixos/modules/services/web-apps/galene.nix2
-rw-r--r--nixos/modules/services/web-apps/gotosocial.md64
-rw-r--r--nixos/modules/services/web-apps/gotosocial.nix173
-rw-r--r--nixos/modules/services/web-apps/grocy.md66
-rw-r--r--nixos/modules/services/web-apps/grocy.nix6
-rw-r--r--nixos/modules/services/web-apps/grocy.xml77
-rw-r--r--nixos/modules/services/web-apps/guacamole-client.nix60
-rw-r--r--nixos/modules/services/web-apps/guacamole-server.nix83
-rw-r--r--nixos/modules/services/web-apps/hedgedoc.nix11
-rw-r--r--nixos/modules/services/web-apps/hledger-web.nix2
-rw-r--r--nixos/modules/services/web-apps/ihatemoney/default.nix153
-rw-r--r--nixos/modules/services/web-apps/invidious.nix6
-rw-r--r--nixos/modules/services/web-apps/jirafeau.nix2
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.md45
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.nix11
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.xml55
-rw-r--r--nixos/modules/services/web-apps/kasmweb/default.nix275
-rw-r--r--nixos/modules/services/web-apps/kasmweb/initialize_kasmweb.sh114
-rw-r--r--nixos/modules/services/web-apps/kavita.nix83
-rw-r--r--nixos/modules/services/web-apps/keycloak.md141
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix2
-rw-r--r--nixos/modules/services/web-apps/keycloak.xml202
-rw-r--r--nixos/modules/services/web-apps/lemmy.nix127
-rw-r--r--nixos/modules/services/web-apps/lemmy.xml51
-rw-r--r--nixos/modules/services/web-apps/limesurvey.nix35
-rw-r--r--nixos/modules/services/web-apps/mainsail.nix66
-rw-r--r--nixos/modules/services/web-apps/mastodon.nix154
-rw-r--r--nixos/modules/services/web-apps/matomo-doc.xml107
-rw-r--r--nixos/modules/services/web-apps/matomo.md77
-rw-r--r--nixos/modules/services/web-apps/matomo.nix2
-rw-r--r--nixos/modules/services/web-apps/mattermost.nix20
-rw-r--r--nixos/modules/services/web-apps/mediawiki.nix175
-rw-r--r--nixos/modules/services/web-apps/monica.nix468
-rw-r--r--nixos/modules/services/web-apps/moodle.nix2
-rw-r--r--nixos/modules/services/web-apps/netbox.nix206
-rw-r--r--nixos/modules/services/web-apps/nextcloud-notify_push.nix123
-rw-r--r--nixos/modules/services/web-apps/nextcloud.md227
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix175
-rw-r--r--nixos/modules/services/web-apps/nextcloud.xml305
-rw-r--r--nixos/modules/services/web-apps/nifi.nix4
-rw-r--r--nixos/modules/services/web-apps/onlyoffice.nix7
-rw-r--r--nixos/modules/services/web-apps/openvscode-server.nix212
-rw-r--r--nixos/modules/services/web-apps/outline.nix66
-rw-r--r--nixos/modules/services/web-apps/peertube.nix12
-rw-r--r--nixos/modules/services/web-apps/photoprism.nix155
-rw-r--r--nixos/modules/services/web-apps/pict-rs.md1
-rw-r--r--nixos/modules/services/web-apps/pict-rs.nix8
-rw-r--r--nixos/modules/services/web-apps/pict-rs.xml162
-rw-r--r--nixos/modules/services/web-apps/pixelfed.nix483
-rw-r--r--nixos/modules/services/web-apps/plausible.md35
-rw-r--r--nixos/modules/services/web-apps/plausible.nix23
-rw-r--r--nixos/modules/services/web-apps/plausible.xml51
-rw-r--r--nixos/modules/services/web-apps/powerdns-admin.nix3
-rw-r--r--nixos/modules/services/web-apps/restya-board.nix4
-rw-r--r--nixos/modules/services/web-apps/sftpgo.nix375
-rw-r--r--nixos/modules/services/web-apps/snipe-it.nix9
-rw-r--r--nixos/modules/services/web-apps/tt-rss.nix2
-rw-r--r--nixos/modules/services/web-apps/vikunja.nix8
-rw-r--r--nixos/modules/services/web-apps/wiki-js.nix10
-rw-r--r--nixos/modules/services/web-apps/wordpress.nix176
-rw-r--r--nixos/modules/services/web-apps/writefreely.nix11
-rw-r--r--nixos/modules/services/web-servers/caddy/default.nix61
-rw-r--r--nixos/modules/services/web-servers/fcgiwrap.nix2
-rw-r--r--nixos/modules/services/web-servers/garage-doc.xml139
-rw-r--r--nixos/modules/services/web-servers/garage.md96
-rw-r--r--nixos/modules/services/web-servers/garage.nix11
-rw-r--r--nixos/modules/services/web-servers/jboss/builder.sh1
-rw-r--r--nixos/modules/services/web-servers/keter/default.nix79
-rw-r--r--nixos/modules/services/web-servers/lighttpd/default.nix10
-rw-r--r--nixos/modules/services/web-servers/minio.nix79
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix342
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix77
-rw-r--r--nixos/modules/services/web-servers/rustus.nix252
-rw-r--r--nixos/modules/services/web-servers/stargazer.nix226
-rw-r--r--nixos/modules/services/web-servers/static-web-server.nix68
-rw-r--r--nixos/modules/services/web-servers/tomcat.nix6
-rw-r--r--nixos/modules/services/web-servers/traefik.nix25
-rw-r--r--nixos/modules/services/web-servers/ttyd.nix11
-rw-r--r--nixos/modules/services/web-servers/unit/default.nix2
-rw-r--r--nixos/modules/services/web-servers/varnish/default.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/budgie.nix244
-rw-r--r--nixos/modules/services/x11/desktop-managers/cde.nix4
-rw-r--r--nixos/modules/services/x11/desktop-managers/cinnamon.nix31
-rw-r--r--nixos/modules/services/x11/desktop-managers/deepin.nix208
-rw-r--r--nixos/modules/services/x11/desktop-managers/default.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome.md167
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome.nix6
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome.xml253
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.md74
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix26
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.xml120
-rw-r--r--nixos/modules/services/x11/desktop-managers/phosh.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix259
-rw-r--r--nixos/modules/services/x11/display-managers/account-service-util.nix4
-rw-r--r--nixos/modules/services/x11/display-managers/default.nix35
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix6
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix4
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix21
-rw-r--r--nixos/modules/services/x11/extra-layouts.nix6
-rw-r--r--nixos/modules/services/x11/gdk-pixbuf.nix2
-rw-r--r--nixos/modules/services/x11/hardware/libinput.nix5
-rw-r--r--nixos/modules/services/x11/picom.nix2
-rw-r--r--nixos/modules/services/x11/window-managers/default.nix1
-rw-r--r--nixos/modules/services/x11/window-managers/dk.nix27
-rw-r--r--nixos/modules/services/x11/window-managers/herbstluftwm.nix2
-rw-r--r--nixos/modules/services/x11/window-managers/katriawm.nix4
-rw-r--r--nixos/modules/services/x11/window-managers/nimdow.nix23
-rw-r--r--nixos/modules/services/x11/window-managers/qtile.nix45
-rw-r--r--nixos/modules/services/x11/window-managers/stumpwm.nix4
-rw-r--r--nixos/modules/services/x11/xserver.nix48
-rw-r--r--nixos/modules/system/activation/activatable-system.nix92
-rw-r--r--nixos/modules/system/activation/activation-script.nix30
-rw-r--r--nixos/modules/system/activation/bootspec.cue27
-rw-r--r--nixos/modules/system/activation/bootspec.nix50
-rwxr-xr-xnixos/modules/system/activation/switch-to-configuration.pl38
-rw-r--r--nixos/modules/system/activation/top-level.nix137
-rw-r--r--nixos/modules/system/boot/binfmt.nix25
-rw-r--r--nixos/modules/system/boot/grow-partition.nix5
-rw-r--r--nixos/modules/system/boot/initrd-network.nix6
-rw-r--r--nixos/modules/system/boot/initrd-openvpn.nix21
-rw-r--r--nixos/modules/system/boot/initrd-ssh.nix74
-rw-r--r--nixos/modules/system/boot/kernel.nix44
-rw-r--r--nixos/modules/system/boot/loader/external/external.md2
-rw-r--r--nixos/modules/system/boot/loader/external/external.nix4
-rw-r--r--nixos/modules/system/boot/loader/external/external.xml41
-rw-r--r--nixos/modules/system/boot/loader/grub/grub.nix148
-rw-r--r--nixos/modules/system/boot/loader/grub/install-grub.pl502
-rw-r--r--nixos/modules/system/boot/loader/grub/ipxe.nix6
-rw-r--r--nixos/modules/system/boot/loader/grub/memtest.nix14
-rw-r--r--nixos/modules/system/boot/loader/init-script/init-script-builder.sh6
-rw-r--r--nixos/modules/system/boot/loader/init-script/init-script.nix1
-rw-r--r--nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix64
-rwxr-xr-xnixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py79
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix8
-rw-r--r--nixos/modules/system/boot/luksroot.nix84
-rw-r--r--nixos/modules/system/boot/modprobe.nix2
-rw-r--r--nixos/modules/system/boot/networkd.nix1358
-rw-r--r--nixos/modules/system/boot/plymouth.nix61
-rw-r--r--nixos/modules/system/boot/resolved.nix4
-rw-r--r--nixos/modules/system/boot/stage-1-init.sh57
-rw-r--r--nixos/modules/system/boot/stage-1.nix33
-rwxr-xr-xnixos/modules/system/boot/stage-2-init.sh2
-rw-r--r--nixos/modules/system/boot/stage-2.nix1
-rw-r--r--nixos/modules/system/boot/stratisroot.nix64
-rw-r--r--nixos/modules/system/boot/systemd.nix17
-rw-r--r--nixos/modules/system/boot/systemd/coredump.nix16
-rw-r--r--nixos/modules/system/boot/systemd/homed.nix43
-rw-r--r--nixos/modules/system/boot/systemd/initrd-secrets.nix4
-rw-r--r--nixos/modules/system/boot/systemd/initrd.nix108
-rw-r--r--nixos/modules/system/boot/systemd/logind.nix125
-rw-r--r--nixos/modules/system/boot/systemd/nspawn.nix2
-rw-r--r--nixos/modules/system/boot/systemd/repart.nix152
-rw-r--r--nixos/modules/system/boot/systemd/user.nix78
-rw-r--r--nixos/modules/system/boot/systemd/userdbd.nix18
-rw-r--r--nixos/modules/system/boot/tmp.nix81
-rw-r--r--nixos/modules/system/etc/setup-etc.pl23
-rw-r--r--nixos/modules/tasks/bcache.nix8
-rw-r--r--nixos/modules/tasks/filesystems.nix141
-rw-r--r--nixos/modules/tasks/filesystems/bcachefs.nix16
-rw-r--r--nixos/modules/tasks/filesystems/btrfs.nix2
-rw-r--r--nixos/modules/tasks/filesystems/envfs.nix23
-rw-r--r--nixos/modules/tasks/filesystems/erofs.nix21
-rw-r--r--nixos/modules/tasks/filesystems/f2fs.nix5
-rw-r--r--nixos/modules/tasks/filesystems/squashfs.nix13
-rw-r--r--nixos/modules/tasks/filesystems/vfat.nix2
-rw-r--r--nixos/modules/tasks/filesystems/zfs.nix85
-rw-r--r--nixos/modules/tasks/lvm.nix17
-rw-r--r--nixos/modules/tasks/network-interfaces-scripted.nix12
-rw-r--r--nixos/modules/tasks/network-interfaces-systemd.nix304
-rw-r--r--nixos/modules/tasks/network-interfaces.nix5
-rw-r--r--nixos/modules/tasks/swraid.nix8
-rw-r--r--nixos/modules/testing/test-instrumentation.nix14
-rw-r--r--nixos/modules/virtualisation/amazon-ec2-amis.nix50
-rw-r--r--nixos/modules/virtualisation/amazon-image.nix8
-rw-r--r--nixos/modules/virtualisation/amazon-options.nix13
-rw-r--r--nixos/modules/virtualisation/azure-agent-entropy.patch17
-rw-r--r--nixos/modules/virtualisation/azure-agent.nix216
-rw-r--r--nixos/modules/virtualisation/azure-common.nix7
-rw-r--r--nixos/modules/virtualisation/brightbox-image.nix2
-rw-r--r--nixos/modules/virtualisation/cloudstack-config.nix2
-rw-r--r--nixos/modules/virtualisation/containers.nix4
-rw-r--r--nixos/modules/virtualisation/cri-o.nix15
-rw-r--r--nixos/modules/virtualisation/digital-ocean-config.nix2
-rw-r--r--nixos/modules/virtualisation/docker.nix4
-rw-r--r--nixos/modules/virtualisation/ec2-metadata-fetcher.sh7
-rw-r--r--nixos/modules/virtualisation/google-compute-config.nix20
-rw-r--r--nixos/modules/virtualisation/libvirtd.nix23
-rw-r--r--nixos/modules/virtualisation/linode-config.nix4
-rw-r--r--nixos/modules/virtualisation/linode-image.nix2
-rw-r--r--nixos/modules/virtualisation/lxc-container.nix15
-rw-r--r--nixos/modules/virtualisation/lxd.nix14
-rw-r--r--nixos/modules/virtualisation/multipass.nix61
-rw-r--r--nixos/modules/virtualisation/nixos-containers.nix31
-rw-r--r--nixos/modules/virtualisation/oci-containers.nix5
-rw-r--r--nixos/modules/virtualisation/openstack-config.nix4
-rw-r--r--nixos/modules/virtualisation/openstack-options.nix4
-rw-r--r--nixos/modules/virtualisation/parallels-guest.nix1
-rw-r--r--nixos/modules/virtualisation/podman/default.nix63
-rw-r--r--nixos/modules/virtualisation/podman/dnsname.nix36
-rw-r--r--nixos/modules/virtualisation/proxmox-image.nix40
-rw-r--r--nixos/modules/virtualisation/qemu-vm.nix641
-rw-r--r--nixos/modules/virtualisation/rosetta.nix10
-rw-r--r--nixos/modules/virtualisation/virtualbox-image.nix63
-rw-r--r--nixos/modules/virtualisation/xen-domU.nix1
757 files changed, 34291 insertions, 17265 deletions
diff --git a/nixos/modules/config/console.nix b/nixos/modules/config/console.nix
index f5db5dc5dfc..1e8bb78f302 100644
--- a/nixos/modules/config/console.nix
+++ b/nixos/modules/config/console.nix
@@ -21,7 +21,7 @@ let
   # Sadly, systemd-vconsole-setup doesn't support binary keymaps.
   vconsoleConf = pkgs.writeText "vconsole.conf" ''
     KEYMAP=${cfg.keyMap}
-    FONT=${cfg.font}
+    ${optionalString (cfg.font != null) "FONT=${cfg.font}"}
   '';
 
   consoleEnv = kbd: pkgs.buildEnv {
@@ -45,14 +45,19 @@ in
     };
 
     font = mkOption {
-      type = with types; either str path;
-      default = "Lat2-Terminus16";
+      type = with types; nullOr (either str path);
+      default = null;
       example = "LatArCyrHeb-16";
       description = mdDoc ''
-        The font used for the virtual consoles.  Leave empty to use
-        whatever the {command}`setfont` program considers the
-        default font.
-        Can be either a font name or a path to a PSF font file.
+        The font used for the virtual consoles.
+        Can be `null`, a font name, or a path to a PSF font file.
+
+        Use `null` to let the kernel choose a built-in font.
+        The default is 8x16, and, as of Linux 5.3, Terminus 32 bold for display
+        resolutions of 2560x1080 and higher.
+        These fonts cover the [IBM437][] character set.
+
+        [IBM437]: https://en.wikipedia.org/wiki/Code_page_437
       '';
     };
 
@@ -151,7 +156,7 @@ in
           printf "\033%%${if isUnicode then "G" else "@"}" >> /dev/console
           loadkmap < ${optimizedKeymap}
 
-          ${optionalString cfg.earlySetup ''
+          ${optionalString (cfg.earlySetup && cfg.font != null) ''
             setfont -C /dev/console $extraUtils/share/consolefonts/font.psf
           ''}
         '');
@@ -168,7 +173,7 @@ in
           "${config.boot.initrd.systemd.package.kbd}/bin/setfont"
           "${config.boot.initrd.systemd.package.kbd}/bin/loadkeys"
           "${config.boot.initrd.systemd.package.kbd.gzip}/bin/gzip" # Fonts and keyboard layouts are compressed
-        ] ++ optionals (hasPrefix builtins.storeDir cfg.font) [
+        ] ++ optionals (cfg.font != null && hasPrefix builtins.storeDir cfg.font) [
           "${cfg.font}"
         ] ++ optionals (hasPrefix builtins.storeDir cfg.keyMap) [
           "${cfg.keyMap}"
@@ -195,7 +200,7 @@ in
         ];
       })
 
-      (mkIf (cfg.earlySetup && !config.boot.initrd.systemd.enable) {
+      (mkIf (cfg.earlySetup && cfg.font != null && !config.boot.initrd.systemd.enable) {
         boot.initrd.extraUtilsCommands = ''
           mkdir -p $out/share/consolefonts
           ${if substring 0 1 cfg.font == "/" then ''
diff --git a/nixos/modules/config/fonts/fontconfig.nix b/nixos/modules/config/fonts/fontconfig.nix
index f9c6e5be226..2eee5cd34d0 100644
--- a/nixos/modules/config/fonts/fontconfig.nix
+++ b/nixos/modules/config/fonts/fontconfig.nix
@@ -7,6 +7,19 @@ This module generates a package containing configuration files and link it in /e
 Fontconfig reads files in folder name / file name order, so the number prepended to the configuration file name decide the order of parsing.
 Low number means high priority.
 
+NOTE: Please take extreme care when adjusting the default settings of this module.
+People care a lot, and I mean A LOT, about their font rendering, and you will be
+The Person That Broke It if it changes in a way people don't like.
+
+See prior art:
+- https://github.com/NixOS/nixpkgs/pull/194594
+- https://github.com/NixOS/nixpkgs/pull/222236
+- https://github.com/NixOS/nixpkgs/pull/222689
+
+And do not repeat our mistakes.
+
+- @K900, March 2023
+
 */
 
 { config, pkgs, lib, ... }:
@@ -64,18 +77,6 @@ let
         <edit mode="append" name="autohint">
           ${fcBool cfg.hinting.autohint}
         </edit>
-        <edit mode="append" name="hintstyle">
-          <const>${cfg.hinting.style}</const>
-        </edit>
-        <edit mode="append" name="antialias">
-          ${fcBool cfg.antialias}
-        </edit>
-        <edit mode="append" name="rgba">
-          <const>${cfg.subpixel.rgba}</const>
-        </edit>
-        <edit mode="append" name="lcdfilter">
-          <const>lcd${cfg.subpixel.lcdfilter}</const>
-        </edit>
       </match>
 
     </fontconfig>
@@ -164,6 +165,13 @@ let
     </fontconfig>
   '';
 
+  # Replace default linked config with a different variant
+  replaceDefaultConfig = defaultConfig: newConfig: ''
+    rm $dst/${defaultConfig}
+    ln -s ${pkg.out}/share/fontconfig/conf.avail/${newConfig} \
+          $dst/
+  '';
+
   # fontconfig configuration package
   confPkg = pkgs.runCommand "fontconfig-conf" {
     preferLocalBuild = true;
@@ -183,6 +191,26 @@ let
     ln -s ${pkg.out}/etc/fonts/conf.d/*.conf \
           $dst/
 
+    ${optionalString (!cfg.antialias)
+      (replaceDefaultConfig "10-yes-antialias.conf"
+        "10-no-antialias.conf")
+    }
+
+    ${optionalString (cfg.hinting.style != "slight")
+      (replaceDefaultConfig "10-hinting-slight.conf"
+        "10-hinting-${cfg.hinting.style}.conf")
+    }
+
+    ${optionalString (cfg.subpixel.rgba != "none")
+      (replaceDefaultConfig "10-sub-pixel-none.conf"
+        "10-sub-pixel-${cfg.subpixel.rgba}.conf")
+    }
+
+    ${optionalString (cfg.subpixel.lcdfilter != "default")
+      (replaceDefaultConfig "11-lcdfilter-default.conf"
+        "11-lcdfilter-${cfg.subpixel.lcdfilter}.conf")
+    }
+
     # 00-nixos-cache.conf
     ln -s ${cacheConf}  $dst/00-nixos-cache.conf
 
@@ -218,6 +246,8 @@ let
     paths = cfg.confPackages;
     ignoreCollisions = true;
   };
+
+  fontconfigNote = "Consider manually configuring fonts.fontconfig according to personal preference.";
 in
 {
   imports = [
@@ -229,6 +259,8 @@ in
     (mkRemovedOptionModule [ "fonts" "fontconfig" "forceAutohint" ] "")
     (mkRemovedOptionModule [ "fonts" "fontconfig" "renderMonoTTFAsBitmap" ] "")
     (mkRemovedOptionModule [ "fonts" "fontconfig" "dpi" ] "Use display server-specific options")
+    (mkRemovedOptionModule [ "hardware" "video" "hidpi" "enable" ] fontconfigNote)
+    (mkRemovedOptionModule [ "fonts" "optimizeForVeryHighDPI" ] fontconfigNote)
   ] ++ lib.forEach [ "enable" "substitutions" "preset" ]
      (opt: lib.mkRemovedOptionModule [ "fonts" "fontconfig" "ultimate" "${opt}" ] ''
        The fonts.fontconfig.ultimate module and configuration is obsolete.
@@ -350,17 +382,25 @@ in
           };
 
           style = mkOption {
-            type = types.enum [ "hintnone" "hintslight" "hintmedium" "hintfull" ];
-            default = "hintslight";
+            type = types.enum ["none" "slight" "medium" "full"];
+            default = "slight";
             description = lib.mdDoc ''
               Hintstyle is the amount of font reshaping done to line up
               to the grid.
 
-              hintslight will make the font more fuzzy to line up to the grid
-              but will be better in retaining font shape, while hintfull will
-              be a crisp font that aligns well to the pixel grid but will lose
-              a greater amount of font shape.
+              slight will make the font more fuzzy to line up to the grid but
+              will be better in retaining font shape, while full will be a
+              crisp font that aligns well to the pixel grid but will lose a
+              greater amount of font shape.
             '';
+            apply =
+              val:
+              let
+                from = "fonts.fontconfig.hinting.style";
+                val' = lib.removePrefix "hint" val;
+                warning = "The option `${from}` contains a deprecated value `${val}`. Use `${val'}` instead.";
+              in
+              lib.warnIf (lib.hasPrefix "hint" val) warning val';
           };
         };
 
@@ -377,7 +417,7 @@ in
         subpixel = {
 
           rgba = mkOption {
-            default = "rgb";
+            default = "none";
             type = types.enum ["rgb" "bgr" "vrgb" "vbgr" "none"];
             description = lib.mdDoc ''
               Subpixel order. The overwhelming majority of displays are
diff --git a/nixos/modules/config/fonts/fontdir.nix b/nixos/modules/config/fonts/fontdir.nix
index 30e0dfe2566..9d41463c947 100644
--- a/nixos/modules/config/fonts/fontdir.nix
+++ b/nixos/modules/config/fonts/fontdir.nix
@@ -8,7 +8,7 @@ let
 
   x11Fonts = pkgs.runCommand "X11-fonts" { preferLocalBuild = true; } ''
     mkdir -p "$out/share/X11/fonts"
-    font_regexp='.*\.\(ttf\|ttc\|otf\|pcf\|pfa\|pfb\|bdf\)\(\.gz\)?'
+    font_regexp='.*\.\(ttf\|ttc\|otb\|otf\|pcf\|pfa\|pfb\|bdf\)\(\.gz\)?'
     find ${toString config.fonts.fonts} -regex "$font_regexp" \
       -exec ln -sf -t "$out/share/X11/fonts" '{}' \;
     cd "$out/share/X11/fonts"
diff --git a/nixos/modules/config/fonts/fonts.nix b/nixos/modules/config/fonts/fonts.nix
index c0619fa31a3..87cf837e7c8 100644
--- a/nixos/modules/config/fonts/fonts.nix
+++ b/nixos/modules/config/fonts/fonts.nix
@@ -3,29 +3,7 @@
 with lib;
 
 let
-  # A scalable variant of the X11 "core" cursor
-  #
-  # If not running a fancy desktop environment, the cursor is likely set to
-  # the default `cursor.pcf` bitmap font. This is 17px wide, so it's very
-  # small and almost invisible on 4K displays.
-  fontcursormisc_hidpi = pkgs.xorg.fontxfree86type1.overrideAttrs (old:
-    let
-      # The scaling constant is 230/96: the scalable `left_ptr` glyph at
-      # about 23 points is rendered as 17px, on a 96dpi display.
-      # Note: the XLFD font size is in decipoints.
-      size = 2.39583 * config.services.xserver.dpi;
-      sizeString = builtins.head (builtins.split "\\." (toString size));
-    in
-    {
-      postInstall = ''
-        alias='cursor -xfree86-cursor-medium-r-normal--0-${sizeString}-0-0-p-0-adobe-fontspecific'
-        echo "$alias" > $out/lib/X11/fonts/Type1/fonts.alias
-      '';
-    });
-
-  hasHidpi =
-    config.hardware.video.hidpi.enable &&
-    config.services.xserver.dpi != null;
+  cfg = config.fonts;
 
   defaultFonts =
     [ pkgs.dejavu_fonts
@@ -35,14 +13,7 @@ let
       pkgs.unifont
       pkgs.noto-fonts-emoji
     ];
-
-  defaultXFonts =
-    [ (if hasHidpi then fontcursormisc_hidpi else pkgs.xorg.fontcursormisc)
-      pkgs.xorg.fontmiscmisc
-    ];
-
 in
-
 {
   imports = [
     (mkRemovedOptionModule [ "fonts" "enableCoreFonts" ] "Use fonts.fonts = [ pkgs.corefonts ]; instead.")
@@ -68,14 +39,9 @@ in
           and families and reasonable coverage of Unicode.
         '';
       };
-
     };
 
   };
 
-  config = mkMerge [
-    { fonts.fonts = mkIf config.fonts.enableDefaultFonts defaultFonts; }
-    { fonts.fonts = mkIf config.services.xserver.enable defaultXFonts; }
-  ];
-
+  config = { fonts.fonts = mkIf cfg.enableDefaultFonts defaultFonts; };
 }
diff --git a/nixos/modules/config/gnu.nix b/nixos/modules/config/gnu.nix
index d06b479e2af..a47d299b226 100644
--- a/nixos/modules/config/gnu.nix
+++ b/nixos/modules/config/gnu.nix
@@ -29,7 +29,6 @@
 
     # GNU GRUB, where available.
     boot.loader.grub.enable = !pkgs.stdenv.isAarch32;
-    boot.loader.grub.version = 2;
 
     # GNU lsh.
     services.openssh.enable = false;
diff --git a/nixos/modules/config/i18n.nix b/nixos/modules/config/i18n.nix
index b1efc00773d..b19d38091e7 100644
--- a/nixos/modules/config/i18n.nix
+++ b/nixos/modules/config/i18n.nix
@@ -66,6 +66,7 @@ with lib;
             (builtins.map (l: (replaceStrings [ "utf8" "utf-8" "UTF8" ] [ "UTF-8" "UTF-8" "UTF-8" ] l) + "/UTF-8") (
               [
                 "C.UTF-8"
+                "en_US.UTF-8"
                 config.i18n.defaultLocale
               ] ++ (attrValues (filterAttrs (n: v: n != "LANGUAGE") config.i18n.extraLocaleSettings))
             ))
diff --git a/nixos/modules/config/malloc.nix b/nixos/modules/config/malloc.nix
index 4db0480b155..3d70e091983 100644
--- a/nixos/modules/config/malloc.nix
+++ b/nixos/modules/config/malloc.nix
@@ -30,7 +30,7 @@ let
 
       systemPlatform = platformMap.${pkgs.stdenv.hostPlatform.system} or (throw "scudo not supported on ${pkgs.stdenv.hostPlatform.system}");
     in {
-      libPath = "${pkgs.llvmPackages_latest.compiler-rt}/lib/linux/libclang_rt.scudo-${systemPlatform}.so";
+      libPath = "${pkgs.llvmPackages_14.compiler-rt}/lib/linux/libclang_rt.scudo-${systemPlatform}.so";
       description = ''
         A user-mode allocator based on LLVM Sanitizer’s CombinedAllocator,
         which aims at providing additional mitigations against heap based
@@ -97,6 +97,7 @@ in
   };
 
   config = mkIf (cfg.provider != "libc") {
+    boot.kernel.sysctl."vm.max_map_count" = mkIf (cfg.provider == "graphene-hardened") (mkDefault 1048576); # TODO: Default vm.max_map_count has been increased system-wide
     environment.etc."ld-nix.so.preload".text = ''
       ${providerLibPath}
     '';
diff --git a/nixos/modules/config/mysql.nix b/nixos/modules/config/mysql.nix
index af20a5e9535..2f13c56f2ae 100644
--- a/nixos/modules/config/mysql.nix
+++ b/nixos/modules/config/mysql.nix
@@ -181,7 +181,7 @@ in
                 example = "pid";
                 description = lib.mdDoc ''
                   The name of the column in the log table to which the pid of the
-                  process utilising the `pam_mysql's` authentication
+                  process utilising the `pam_mysql` authentication
                   service is stored.
                 '';
               };
diff --git a/nixos/modules/config/nix-channel.nix b/nixos/modules/config/nix-channel.nix
new file mode 100644
index 00000000000..557f17d8b3b
--- /dev/null
+++ b/nixos/modules/config/nix-channel.nix
@@ -0,0 +1,70 @@
+/*
+  Manages the things that are needed for a traditional nix-channel based
+  configuration to work.
+
+  See also
+   - ./nix.nix
+   - ./nix-flakes.nix
+ */
+{ config, lib, ... }:
+let
+  inherit (lib)
+    mkIf
+    mkOption
+    stringAfter
+    types
+    ;
+
+  cfg = config.nix;
+
+in
+{
+  options = {
+    nix = {
+      nixPath = mkOption {
+        type = types.listOf types.str;
+        default = [
+          "nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos"
+          "nixos-config=/etc/nixos/configuration.nix"
+          "/nix/var/nix/profiles/per-user/root/channels"
+        ];
+        description = lib.mdDoc ''
+          The default Nix expression search path, used by the Nix
+          evaluator to look up paths enclosed in angle brackets
+          (e.g. `<nixpkgs>`).
+        '';
+      };
+    };
+
+    system = {
+      defaultChannel = mkOption {
+        internal = true;
+        type = types.str;
+        default = "https://nixos.org/channels/nixos-unstable";
+        description = lib.mdDoc "Default NixOS channel to which the root user is subscribed.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.extraInit =
+      ''
+        if [ -e "$HOME/.nix-defexpr/channels" ]; then
+          export NIX_PATH="$HOME/.nix-defexpr/channels''${NIX_PATH:+:$NIX_PATH}"
+        fi
+      '';
+
+    environment.sessionVariables = {
+      NIX_PATH = cfg.nixPath;
+    };
+
+    system.activationScripts.nix-channel = stringAfter [ "etc" "users" ]
+      ''
+        # Subscribe the root user to the NixOS channel by default.
+        if [ ! -e "/root/.nix-channels" ]; then
+            echo "${config.system.defaultChannel} nixos" > "/root/.nix-channels"
+        fi
+      '';
+  };
+}
diff --git a/nixos/modules/config/nix-flakes.nix b/nixos/modules/config/nix-flakes.nix
new file mode 100644
index 00000000000..242d8d3b82b
--- /dev/null
+++ b/nixos/modules/config/nix-flakes.nix
@@ -0,0 +1,95 @@
+/*
+  Manages the flake registry.
+
+  See also
+   - ./nix.nix
+   - ./nix-channel.nix
+ */
+{ config, lib, ... }:
+let
+  inherit (lib)
+    filterAttrs
+    literalExpression
+    mapAttrsToList
+    mkDefault
+    mkIf
+    mkOption
+    types
+    ;
+
+  cfg = config.nix;
+
+in
+{
+  options = {
+    nix = {
+      registry = mkOption {
+        type = types.attrsOf (types.submodule (
+          let
+            referenceAttrs = with types; attrsOf (oneOf [
+              str
+              int
+              bool
+              path
+              package
+            ]);
+          in
+          { config, name, ... }:
+          {
+            options = {
+              from = mkOption {
+                type = referenceAttrs;
+                example = { type = "indirect"; id = "nixpkgs"; };
+                description = lib.mdDoc "The flake reference to be rewritten.";
+              };
+              to = mkOption {
+                type = referenceAttrs;
+                example = { type = "github"; owner = "my-org"; repo = "my-nixpkgs"; };
+                description = lib.mdDoc "The flake reference {option}`from` is rewritten to.";
+              };
+              flake = mkOption {
+                type = types.nullOr types.attrs;
+                default = null;
+                example = literalExpression "nixpkgs";
+                description = lib.mdDoc ''
+                  The flake input {option}`from` is rewritten to.
+                '';
+              };
+              exact = mkOption {
+                type = types.bool;
+                default = true;
+                description = lib.mdDoc ''
+                  Whether the {option}`from` reference needs to match exactly. If set,
+                  a {option}`from` reference like `nixpkgs` does not
+                  match with a reference like `nixpkgs/nixos-20.03`.
+                '';
+              };
+            };
+            config = {
+              from = mkDefault { type = "indirect"; id = name; };
+              to = mkIf (config.flake != null) (mkDefault (
+                {
+                  type = "path";
+                  path = config.flake.outPath;
+                } // filterAttrs
+                  (n: _: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash")
+                  config.flake
+              ));
+            };
+          }
+        ));
+        default = { };
+        description = lib.mdDoc ''
+          A system-wide flake registry.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."nix/registry.json".text = builtins.toJSON {
+      version = 2;
+      flakes = mapAttrsToList (n: v: { inherit (v) from to exact; }) cfg.registry;
+    };
+  };
+}
diff --git a/nixos/modules/config/nix-remote-build.nix b/nixos/modules/config/nix-remote-build.nix
new file mode 100644
index 00000000000..98c8fc06d2e
--- /dev/null
+++ b/nixos/modules/config/nix-remote-build.nix
@@ -0,0 +1,226 @@
+/*
+  Manages the remote build configuration, /etc/nix/machines
+
+  See also
+   - ./nix.nix
+   - nixos/modules/services/system/nix-daemon.nix
+ */
+{ config, lib, ... }:
+
+let
+  inherit (lib)
+    any
+    concatMapStrings
+    concatStringsSep
+    filter
+    getVersion
+    mkIf
+    mkMerge
+    mkOption
+    optional
+    optionalString
+    types
+    versionAtLeast
+    ;
+
+  cfg = config.nix;
+
+  nixPackage = cfg.package.out;
+
+  isNixAtLeast = versionAtLeast (getVersion nixPackage);
+
+  buildMachinesText =
+    concatMapStrings
+      (machine:
+        (concatStringsSep " " ([
+          "${optionalString (machine.protocol != null) "${machine.protocol}://"}${optionalString (machine.sshUser != null) "${machine.sshUser}@"}${machine.hostName}"
+          (if machine.system != null then machine.system else if machine.systems != [ ] then concatStringsSep "," machine.systems else "-")
+          (if machine.sshKey != null then machine.sshKey else "-")
+          (toString machine.maxJobs)
+          (toString machine.speedFactor)
+          (let res = (machine.supportedFeatures ++ machine.mandatoryFeatures);
+            in if (res == []) then "-" else (concatStringsSep "," res))
+          (let res = machine.mandatoryFeatures;
+            in if (res == []) then "-" else (concatStringsSep "," machine.mandatoryFeatures))
+        ]
+        ++ optional (isNixAtLeast "2.4pre") (if machine.publicHostKey != null then machine.publicHostKey else "-")))
+        + "\n"
+      )
+      cfg.buildMachines;
+
+in
+{
+  options = {
+    nix = {
+      buildMachines = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            hostName = mkOption {
+              type = types.str;
+              example = "nixbuilder.example.org";
+              description = lib.mdDoc ''
+                The hostname of the build machine.
+              '';
+            };
+            protocol = mkOption {
+              type = types.enum [ null "ssh" "ssh-ng" ];
+              default = "ssh";
+              example = "ssh-ng";
+              description = lib.mdDoc ''
+                The protocol used for communicating with the build machine.
+                Use `ssh-ng` if your remote builder and your
+                local Nix version support that improved protocol.
+
+                Use `null` when trying to change the special localhost builder
+                without a protocol which is for example used by hydra.
+              '';
+            };
+            system = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "x86_64-linux";
+              description = lib.mdDoc ''
+                The system type the build machine can execute derivations on.
+                Either this attribute or {var}`systems` must be
+                present, where {var}`system` takes precedence if
+                both are set.
+              '';
+            };
+            systems = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "x86_64-linux" "aarch64-linux" ];
+              description = lib.mdDoc ''
+                The system types the build machine can execute derivations on.
+                Either this attribute or {var}`system` must be
+                present, where {var}`system` takes precedence if
+                both are set.
+              '';
+            };
+            sshUser = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "builder";
+              description = lib.mdDoc ''
+                The username to log in as on the remote host. This user must be
+                able to log in and run nix commands non-interactively. It must
+                also be privileged to build derivations, so must be included in
+                {option}`nix.settings.trusted-users`.
+              '';
+            };
+            sshKey = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "/root/.ssh/id_buildhost_builduser";
+              description = lib.mdDoc ''
+                The path to the SSH private key with which to authenticate on
+                the build machine. The private key must not have a passphrase.
+                If null, the building user (root on NixOS machines) must have an
+                appropriate ssh configuration to log in non-interactively.
+
+                Note that for security reasons, this path must point to a file
+                in the local filesystem, *not* to the nix store.
+              '';
+            };
+            maxJobs = mkOption {
+              type = types.int;
+              default = 1;
+              description = lib.mdDoc ''
+                The number of concurrent jobs the build machine supports. The
+                build machine will enforce its own limits, but this allows hydra
+                to schedule better since there is no work-stealing between build
+                machines.
+              '';
+            };
+            speedFactor = mkOption {
+              type = types.int;
+              default = 1;
+              description = lib.mdDoc ''
+                The relative speed of this builder. This is an arbitrary integer
+                that indicates the speed of this builder, relative to other
+                builders. Higher is faster.
+              '';
+            };
+            mandatoryFeatures = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "big-parallel" ];
+              description = lib.mdDoc ''
+                A list of features mandatory for this builder. The builder will
+                be ignored for derivations that don't require all features in
+                this list. All mandatory features are automatically included in
+                {var}`supportedFeatures`.
+              '';
+            };
+            supportedFeatures = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "kvm" "big-parallel" ];
+              description = lib.mdDoc ''
+                A list of features supported by this builder. The builder will
+                be ignored for derivations that require features not in this
+                list.
+              '';
+            };
+            publicHostKey = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = lib.mdDoc ''
+                The (base64-encoded) public host key of this builder. The field
+                is calculated via {command}`base64 -w0 /etc/ssh/ssh_host_type_key.pub`.
+                If null, SSH will use its regular known-hosts file when connecting.
+              '';
+            };
+          };
+        });
+        default = [ ];
+        description = lib.mdDoc ''
+          This option lists the machines to be used if distributed builds are
+          enabled (see {option}`nix.distributedBuilds`).
+          Nix will perform derivations on those machines via SSH by copying the
+          inputs to the Nix store on the remote machine, starting the build,
+          then copying the output back to the local Nix store.
+        '';
+      };
+
+      distributedBuilds = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Whether to distribute builds to the machines listed in
+          {option}`nix.buildMachines`.
+        '';
+      };
+    };
+  };
+
+  # distributedBuilds does *not* inhibit /etc/machines generation; caller may
+  # override that nix option.
+  config = mkIf cfg.enable {
+    assertions =
+      let badMachine = m: m.system == null && m.systems == [ ];
+      in
+      [
+        {
+          assertion = !(any badMachine cfg.buildMachines);
+          message = ''
+            At least one system type (via <varname>system</varname> or
+              <varname>systems</varname>) must be set for every build machine.
+              Invalid machine specifications:
+          '' + "      " +
+          (concatStringsSep "\n      "
+            (map (m: m.hostName)
+              (filter (badMachine) cfg.buildMachines)));
+        }
+      ];
+
+    # List of machines for distributed Nix builds
+    environment.etc."nix/machines" =
+      mkIf (cfg.buildMachines != [ ]) {
+        text = buildMachinesText;
+      };
+
+    # Legacy configuration conversion.
+    nix.settings = mkIf (!cfg.distributedBuilds) { builders = null; };
+  };
+}
diff --git a/nixos/modules/config/nix.nix b/nixos/modules/config/nix.nix
new file mode 100644
index 00000000000..cee4f54db0c
--- /dev/null
+++ b/nixos/modules/config/nix.nix
@@ -0,0 +1,379 @@
+/*
+  Manages /etc/nix.conf.
+
+  See also
+   - ./nix-channel.nix
+   - ./nix-flakes.nix
+   - ./nix-remote-build.nix
+   - nixos/modules/services/system/nix-daemon.nix
+ */
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib)
+    concatStringsSep
+    boolToString
+    escape
+    floatToString
+    getVersion
+    isBool
+    isDerivation
+    isFloat
+    isInt
+    isList
+    isString
+    literalExpression
+    mapAttrsToList
+    mkAfter
+    mkDefault
+    mkIf
+    mkOption
+    mkRenamedOptionModuleWith
+    optionalString
+    optionals
+    strings
+    systems
+    toPretty
+    types
+    versionAtLeast
+    ;
+
+  cfg = config.nix;
+
+  nixPackage = cfg.package.out;
+
+  isNixAtLeast = versionAtLeast (getVersion nixPackage);
+
+  legacyConfMappings = {
+    useSandbox = "sandbox";
+    buildCores = "cores";
+    maxJobs = "max-jobs";
+    sandboxPaths = "extra-sandbox-paths";
+    binaryCaches = "substituters";
+    trustedBinaryCaches = "trusted-substituters";
+    binaryCachePublicKeys = "trusted-public-keys";
+    autoOptimiseStore = "auto-optimise-store";
+    requireSignedBinaryCaches = "require-sigs";
+    trustedUsers = "trusted-users";
+    allowedUsers = "allowed-users";
+    systemFeatures = "system-features";
+  };
+
+  semanticConfType = with types;
+    let
+      confAtom = nullOr
+        (oneOf [
+          bool
+          int
+          float
+          str
+          path
+          package
+        ]) // {
+        description = "Nix config atom (null, bool, int, float, str, path or package)";
+      };
+    in
+    attrsOf (either confAtom (listOf confAtom));
+
+  nixConf =
+    assert isNixAtLeast "2.2";
+    let
+
+      mkValueString = v:
+        if v == null then ""
+        else if isInt v then toString v
+        else if isBool v then boolToString v
+        else if isFloat v then floatToString v
+        else if isList v then toString v
+        else if isDerivation v then toString v
+        else if builtins.isPath v then toString v
+        else if isString v then v
+        else if strings.isConvertibleWithToString v then toString v
+        else abort "The nix conf value: ${toPretty {} v} can not be encoded";
+
+      mkKeyValue = k: v: "${escape [ "=" ] k} = ${mkValueString v}";
+
+      mkKeyValuePairs = attrs: concatStringsSep "\n" (mapAttrsToList mkKeyValue attrs);
+
+    in
+    pkgs.writeTextFile {
+      name = "nix.conf";
+      text = ''
+        # WARNING: this file is generated from the nix.* options in
+        # your NixOS configuration, typically
+        # /etc/nixos/configuration.nix.  Do not edit it!
+        ${mkKeyValuePairs cfg.settings}
+        ${cfg.extraOptions}
+      '';
+      checkPhase = lib.optionalString cfg.checkConfig (
+        if pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform then ''
+          echo "Ignoring validation for cross-compilation"
+        ''
+        else ''
+          echo "Validating generated nix.conf"
+          ln -s $out ./nix.conf
+          set -e
+          set +o pipefail
+          NIX_CONF_DIR=$PWD \
+            ${cfg.package}/bin/nix show-config ${optionalString (isNixAtLeast "2.3pre") "--no-net"} \
+              ${optionalString (isNixAtLeast "2.4pre") "--option experimental-features nix-command"} \
+            |& sed -e 's/^warning:/error:/' \
+            | (! grep '${if cfg.checkAllErrors then "^error:" else "^error: unknown setting"}')
+          set -o pipefail
+        '');
+    };
+
+in
+{
+  imports = [
+    (mkRenamedOptionModuleWith { sinceRelease = 2003; from = [ "nix" "useChroot" ]; to = [ "nix" "useSandbox" ]; })
+    (mkRenamedOptionModuleWith { sinceRelease = 2003; from = [ "nix" "chrootDirs" ]; to = [ "nix" "sandboxPaths" ]; })
+  ] ++
+    mapAttrsToList
+      (oldConf: newConf:
+        mkRenamedOptionModuleWith {
+          sinceRelease = 2205;
+          from = [ "nix" oldConf ];
+          to = [ "nix" "settings" newConf ];
+      })
+      legacyConfMappings;
+
+  options = {
+    nix = {
+      checkConfig = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc ''
+          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.
+        '';
+      };
+
+      extraOptions = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          keep-outputs = true
+          keep-derivations = true
+        '';
+        description = lib.mdDoc "Additional text appended to {file}`nix.conf`.";
+      };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = semanticConfType;
+
+          options = {
+            max-jobs = mkOption {
+              type = types.either types.int (types.enum [ "auto" ]);
+              default = "auto";
+              example = 64;
+              description = lib.mdDoc ''
+                This option defines the maximum number of jobs that Nix will try to
+                build in parallel. The default is auto, which means it will use all
+                available logical cores. It is recommend to set it to the total
+                number of logical cores in your system (e.g., 16 for two CPUs with 4
+                cores each and hyper-threading).
+              '';
+            };
+
+            auto-optimise-store = mkOption {
+              type = types.bool;
+              default = false;
+              example = true;
+              description = lib.mdDoc ''
+                If set to true, Nix automatically detects files in the store that have
+                identical contents, and replaces them with hard links to a single copy.
+                This saves disk space. If set to false (the default), you can still run
+                nix-store --optimise to get rid of duplicate files.
+              '';
+            };
+
+            cores = mkOption {
+              type = types.int;
+              default = 0;
+              example = 64;
+              description = lib.mdDoc ''
+                This option defines the maximum number of concurrent tasks during
+                one build. It affects, e.g., -j option for make.
+                The special value 0 means that the builder should use all
+                available CPU cores in the system. Some builds may become
+                non-deterministic with this option; use with care! Packages will
+                only be affected if enableParallelBuilding is set for them.
+              '';
+            };
+
+            sandbox = mkOption {
+              type = types.either types.bool (types.enum [ "relaxed" ]);
+              default = true;
+              description = lib.mdDoc ''
+                If set, Nix will perform builds in a sandboxed environment that it
+                will set up automatically for each build. This prevents impurities
+                in builds by disallowing access to dependencies outside of the Nix
+                store by using network and mount namespaces in a chroot environment.
+
+                This is enabled by default even though it has a possible performance
+                impact due to the initial setup time of a sandbox for each build. It
+                doesn't affect derivation hashes, so changing this option will not
+                trigger a rebuild of packages.
+
+                When set to "relaxed", this option permits derivations that set
+                `__noChroot = true;` to run outside of the sandboxed environment.
+                Exercise caution when using this mode of operation! It is intended to
+                be a quick hack when building with packages that are not easily setup
+                to be built reproducibly.
+              '';
+            };
+
+            extra-sandbox-paths = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "/dev" "/proc" ];
+              description = lib.mdDoc ''
+                Directories from the host filesystem to be included
+                in the sandbox.
+              '';
+            };
+
+            substituters = mkOption {
+              type = types.listOf types.str;
+              description = lib.mdDoc ''
+                List of binary cache URLs used to obtain pre-built binaries
+                of Nix packages.
+
+                By default https://cache.nixos.org/ is added.
+              '';
+            };
+
+            trusted-substituters = mkOption {
+              type = types.listOf types.str;
+              default = [ ];
+              example = [ "https://hydra.nixos.org/" ];
+              description = lib.mdDoc ''
+                List of binary cache URLs that non-root users can use (in
+                addition to those specified using
+                {option}`nix.settings.substituters`) by passing
+                `--option binary-caches` to Nix commands.
+              '';
+            };
+
+            require-sigs = mkOption {
+              type = types.bool;
+              default = true;
+              description = lib.mdDoc ''
+                If enabled (the default), Nix will only download binaries from binary caches if
+                they are cryptographically signed with any of the keys listed in
+                {option}`nix.settings.trusted-public-keys`. If disabled, signatures are neither
+                required nor checked, so it's strongly recommended that you use only
+                trustworthy caches and https to prevent man-in-the-middle attacks.
+              '';
+            };
+
+            trusted-public-keys = mkOption {
+              type = types.listOf types.str;
+              example = [ "hydra.nixos.org-1:CNHJZBh9K4tP3EKF6FkkgeVYsS3ohTl+oS0Qa8bezVs=" ];
+              description = lib.mdDoc ''
+                List of public keys used to sign binary caches. If
+                {option}`nix.settings.trusted-public-keys` is enabled,
+                then Nix will use a binary from a binary cache if and only
+                if it is signed by *any* of the keys
+                listed here. By default, only the key for
+                `cache.nixos.org` is included.
+              '';
+            };
+
+            trusted-users = mkOption {
+              type = types.listOf types.str;
+              default = [ "root" ];
+              example = [ "root" "alice" "@wheel" ];
+              description = lib.mdDoc ''
+                A list of names of users that have additional rights when
+                connecting to the Nix daemon, such as the ability to specify
+                additional binary caches, or to import unsigned NARs. You
+                can also specify groups by prefixing them with
+                `@`; for instance,
+                `@wheel` means all users in the wheel
+                group.
+              '';
+            };
+
+            system-features = mkOption {
+              type = types.listOf types.str;
+              example = [ "kvm" "big-parallel" "gccarch-skylake" ];
+              description = lib.mdDoc ''
+                The set of features supported by the machine. Derivations
+                can express dependencies on system features through the
+                `requiredSystemFeatures` attribute.
+
+                By default, pseudo-features `nixos-test`, `benchmark`,
+                and `big-parallel` used in Nixpkgs are set, `kvm`
+                is also included if it is available.
+              '';
+            };
+
+            allowed-users = mkOption {
+              type = types.listOf types.str;
+              default = [ "*" ];
+              example = [ "@wheel" "@builders" "alice" "bob" ];
+              description = lib.mdDoc ''
+                A list of names of users (separated by whitespace) that are
+                allowed to connect to the Nix daemon. As with
+                {option}`nix.settings.trusted-users`, you can specify groups by
+                prefixing them with `@`. Also, you can
+                allow all users by specifying `*`. The
+                default is `*`. Note that trusted users are
+                always allowed to connect.
+              '';
+            };
+          };
+        };
+        default = { };
+        example = literalExpression ''
+          {
+            use-sandbox = true;
+            show-trace = true;
+
+            system-features = [ "big-parallel" "kvm" "recursive-nix" ];
+            sandbox-paths = { "/bin/sh" = "''${pkgs.busybox-sandbox-shell.out}/bin/busybox"; };
+          }
+        '';
+        description = lib.mdDoc ''
+          Configuration for Nix, see
+          <https://nixos.org/manual/nix/stable/command-ref/conf-file.html> or
+          {manpage}`nix.conf(5)` for available options.
+          The value declared here will be translated directly to the key-value pairs Nix expects.
+
+          You can use {command}`nix-instantiate --eval --strict '<nixpkgs/nixos>' -A config.nix.settings`
+          to view the current value. By default it is empty.
+
+          Nix configurations defined under {option}`nix.*` will be translated and applied to this
+          option. In addition, configuration specified in {option}`nix.extraOptions` will be appended
+          verbatim to the resulting config file.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."nix/nix.conf".source = nixConf;
+    nix.settings = {
+      trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ];
+      substituters = mkAfter [ "https://cache.nixos.org/" ];
+      system-features = mkDefault (
+        [ "nixos-test" "benchmark" "big-parallel" "kvm" ] ++
+        optionals (pkgs.stdenv.hostPlatform ? gcc.arch) (
+          # a builder can run code for `gcc.arch` and inferior architectures
+          [ "gccarch-${pkgs.stdenv.hostPlatform.gcc.arch}" ] ++
+          map (x: "gccarch-${x}") (systems.architectures.inferiors.${pkgs.stdenv.hostPlatform.gcc.arch} or [])
+        )
+      );
+    };
+  };
+}
diff --git a/nixos/modules/config/no-x-libs.nix b/nixos/modules/config/no-x-libs.nix
index 70e265a65a6..676d2abda08 100644
--- a/nixos/modules/config/no-x-libs.nix
+++ b/nixos/modules/config/no-x-libs.nix
@@ -30,16 +30,28 @@ with lib;
       beam = super.beam_nox;
       cairo = super.cairo.override { x11Support = false; };
       dbus = super.dbus.override { x11Support = false; };
-      ffmpeg_4 = super.ffmpeg_4-headless;
-      ffmpeg_5 = super.ffmpeg_5-headless;
+      ffmpeg_4 = super.ffmpeg_4.override { ffmpegVariant = "headless"; };
+      ffmpeg_5 = super.ffmpeg_5.override { ffmpegVariant = "headless"; };
+      # dep of graphviz, libXpm is optional for Xpm support
+      gd = super.gd.override { withXorg = false; };
       gobject-introspection = super.gobject-introspection.override { x11Support = false; };
       gpsd = super.gpsd.override { guiSupport = false; };
+      graphviz = super.graphviz-nox;
+      gst_all_1 = super.gst_all_1 // {
+        gst-plugins-bad = super.gst_all_1.gst-plugins-bad.override { guiSupport = false; };
+        gst-plugins-base = super.gst_all_1.gst-plugins-base.override { enableWayland = false; enableX11 = false; };
+        gst-plugins-good = super.gst_all_1.gst-plugins-good.override { enableX11 = false; };
+      };
       imagemagick = super.imagemagick.override { libX11Support = false; libXtSupport = false; };
       imagemagickBig = super.imagemagickBig.override { libX11Support = false; libXtSupport = false; };
-      libextractor = super.libextractor.override { gstreamerSupport = false; gtkSupport = false; };
+      libdevil = super.libdevil-nox;
+      libextractor = super.libextractor.override { gtkSupport = false; };
       libva = super.libva-minimal;
       limesuite = super.limesuite.override { withGui = false; };
+      mc = super.mc.override { x11Support = false; };
+      mpv-unwrapped = super.mpv-unwrapped.override { sdl2Support = false; x11Support = false; waylandSupport = false; };
       msmtp = super.msmtp.override { withKeyring = false; };
+      neofetch = super.neofetch.override { x11Support = false; };
       networkmanager-fortisslvpn = super.networkmanager-fortisslvpn.override { withGnome = false; };
       networkmanager-iodine = super.networkmanager-iodine.override { withGnome = false; };
       networkmanager-l2tp = super.networkmanager-l2tp.override { withGnome = false; };
@@ -47,9 +59,18 @@ with lib;
       networkmanager-openvpn = super.networkmanager-openvpn.override { withGnome = false; };
       networkmanager-sstp = super.networkmanager-vpnc.override { withGnome = false; };
       networkmanager-vpnc = super.networkmanager-vpnc.override { withGnome = false; };
+      pango = super.pango.override { x11Support = false; };
       pinentry = super.pinentry.override { enabledFlavors = [ "curses" "tty" "emacs" ]; withLibsecret = false; };
+      pipewire = super.pipewire.override { x11Support = false; };
       qemu = super.qemu.override { gtkSupport = false; spiceSupport = false; sdlSupport = false; };
       qrencode = super.qrencode.overrideAttrs (_: { doCheck = false; });
+      qt5 = super.qt5.overrideScope (const (super': {
+        qtbase = super'.qtbase.override { withGtk3 = false; };
+      }));
+      stoken = super.stoken.override { withGTK3 = false; };
+      # translateManpages -> perlPackages.po4a -> texlive-combined-basic -> texlive-core-big -> libX11
+      util-linux = super.util-linux.override { translateManpages = false; };
+      vim-full = super.vim-full.override { guiSupport = false; };
       zbar = super.zbar.override { enableVideo = false; withXorg = false; };
     }));
   };
diff --git a/nixos/modules/config/qt5.nix b/nixos/modules/config/qt.nix
index cb3180d7b96..cf4e9621d70 100644
--- a/nixos/modules/config/qt5.nix
+++ b/nixos/modules/config/qt.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
 
-  cfg = config.qt5;
+  cfg = config.qt;
 
   isQGnome = cfg.platformTheme == "gnome" && builtins.elem cfg.style ["adwaita" "adwaita-dark"];
   isQtStyle = cfg.platformTheme == "gtk2" && !(builtins.elem cfg.style ["adwaita" "adwaita-dark"]);
@@ -12,22 +12,34 @@ let
   isLxqt = cfg.platformTheme == "lxqt";
   isKde = cfg.platformTheme == "kde";
 
-  packages = if isQGnome then [ pkgs.qgnomeplatform pkgs.adwaita-qt ]
+  packages =
+    if isQGnome then [
+      pkgs.qgnomeplatform
+      pkgs.adwaita-qt
+      pkgs.qgnomeplatform-qt6
+      pkgs.adwaita-qt6
+    ]
     else if isQtStyle then [ pkgs.libsForQt5.qtstyleplugins ]
-    else if isQt5ct then [ pkgs.libsForQt5.qt5ct ]
+    else if isQt5ct then [ pkgs.libsForQt5.qt5ct pkgs.qt6Packages.qt6ct ]
     else if isLxqt then [ pkgs.lxqt.lxqt-qtplugin pkgs.lxqt.lxqt-config ]
     else if isKde then [ pkgs.libsForQt5.plasma-integration pkgs.libsForQt5.systemsettings ]
-    else throw "`qt5.platformTheme` ${cfg.platformTheme} and `qt5.style` ${cfg.style} are not compatible.";
+    else throw "`qt.platformTheme` ${cfg.platformTheme} and `qt.style` ${cfg.style} are not compatible.";
 
 in
 
 {
   meta.maintainers = [ maintainers.romildo ];
 
+  imports = [
+    (mkRenamedOptionModule ["qt5" "enable" ] ["qt" "enable" ])
+    (mkRenamedOptionModule ["qt5" "platformTheme" ] ["qt" "platformTheme" ])
+    (mkRenamedOptionModule ["qt5" "style" ] ["qt" "style" ])
+  ];
+
   options = {
-    qt5 = {
+    qt = {
 
-      enable = mkEnableOption (lib.mdDoc "Qt5 theming configuration");
+      enable = mkEnableOption (lib.mdDoc "Qt theming configuration");
 
       platformTheme = mkOption {
         type = types.enum [
@@ -40,13 +52,14 @@ in
         example = "gnome";
         relatedPackages = [
           "qgnomeplatform"
+          "qgnomeplatform-qt6"
           ["libsForQt5" "qtstyleplugins"]
           ["libsForQt5" "qt5ct"]
           ["lxqt" "lxqt-qtplugin"]
           ["libsForQt5" "plasma-integration"]
         ];
         description = lib.mdDoc ''
-          Selects the platform theme to use for Qt5 applications.
+          Selects the platform theme to use for Qt applications.
 
           The options are
           - `gtk`: Use GTK theme with [qtstyleplugins](https://github.com/qt/qtstyleplugins)
@@ -71,10 +84,11 @@ in
         example = "adwaita";
         relatedPackages = [
           "adwaita-qt"
+          "adwaita-qt6"
           ["libsForQt5" "qtstyleplugins"]
         ];
         description = lib.mdDoc ''
-          Selects the style to use for Qt5 applications.
+          Selects the style to use for Qt applications.
 
           The options are
           - `adwaita`, `adwaita-dark`: Use Adwaita Qt style with
@@ -88,9 +102,17 @@ in
 
   config = mkIf cfg.enable {
 
-    environment.variables.QT_QPA_PLATFORMTHEME = cfg.platformTheme;
+    environment.variables = {
+      QT_QPA_PLATFORMTHEME = cfg.platformTheme;
+      QT_STYLE_OVERRIDE = mkIf (! (isQt5ct || isLxqt || isKde)) cfg.style;
+    };
 
-    environment.variables.QT_STYLE_OVERRIDE = mkIf (! (isQt5ct || isLxqt || isKde)) cfg.style;
+    environment.profileRelativeSessionVariables = let
+      qtVersions = with pkgs; [ qt5 qt6 ];
+    in {
+      QT_PLUGIN_PATH = map (qt: "/${qt.qtbase.qtPluginPrefix}") qtVersions;
+      QML2_IMPORT_PATH = map (qt: "/${qt.qtbase.qtQmlPrefix}") qtVersions;
+    };
 
     environment.systemPackages = packages;
 
diff --git a/nixos/modules/config/resolvconf.nix b/nixos/modules/config/resolvconf.nix
index 76605a063a4..e9ae4d651d2 100644
--- a/nixos/modules/config/resolvconf.nix
+++ b/nixos/modules/config/resolvconf.nix
@@ -132,13 +132,13 @@ in
             exit 1
           ''
         else configText;
-
-      environment.systemPackages = [ cfg.package ];
     }
 
     (mkIf cfg.enable {
       networking.resolvconf.package = pkgs.openresolv;
 
+      environment.systemPackages = [ cfg.package ];
+
       systemd.services.resolvconf = {
         description = "resolvconf update";
 
diff --git a/nixos/modules/config/stevenblack.nix b/nixos/modules/config/stevenblack.nix
new file mode 100644
index 00000000000..07a0aa339a5
--- /dev/null
+++ b/nixos/modules/config/stevenblack.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) optionals mkOption mkEnableOption types mkIf elem concatStringsSep maintainers mdDoc;
+  cfg = config.networking.stevenblack;
+
+  # needs to be in a specific order
+  activatedHosts = with cfg; [ ]
+    ++ optionals (elem "fakenews" block) [ "fakenews" ]
+    ++ optionals (elem "gambling" block) [ "gambling" ]
+    ++ optionals (elem "porn" block) [ "porn" ]
+    ++ optionals (elem "social" block) [ "social" ];
+
+  hostsPath = "${pkgs.stevenblack-blocklist}/alternates/" + concatStringsSep "-" activatedHosts + "/hosts";
+in
+{
+  options.networking.stevenblack = {
+    enable = mkEnableOption (mdDoc "Enable the stevenblack hosts file blocklist");
+
+    block = mkOption {
+      type = types.listOf (types.enum [ "fakenews" "gambling" "porn" "social" ]);
+      default = [ ];
+      description = mdDoc "Additional blocklist extensions.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.hostFiles = [ ]
+      ++ optionals (activatedHosts != [ ]) [ hostsPath ]
+      ++ optionals (activatedHosts == [ ]) [ "${pkgs.stevenblack-blocklist}/hosts" ];
+  };
+
+  meta.maintainers = [ maintainers.fortuneteller2k maintainers.artturin ];
+}
diff --git a/nixos/modules/config/swap.nix b/nixos/modules/config/swap.nix
index 76a054b100e..0a7e45bffb2 100644
--- a/nixos/modules/config/swap.nix
+++ b/nixos/modules/config/swap.nix
@@ -38,6 +38,34 @@ let
         '';
       };
 
+      keySize = mkOption {
+        default = null;
+        example = "512";
+        type = types.nullOr types.int;
+        description = lib.mdDoc ''
+          Set the encryption key size for the plain device.
+
+          If not specified, the amount of data to read from `source` will be
+          determined by cryptsetup.
+
+          See `cryptsetup-open(8)` for details.
+        '';
+      };
+
+      sectorSize = mkOption {
+        default = null;
+        example = "4096";
+        type = types.nullOr types.int;
+        description = lib.mdDoc ''
+          Set the sector size for the plain encrypted device type.
+
+          If not specified, the default sector size is determined from the
+          underlying block device.
+
+          See `cryptsetup-open(8)` for details.
+        '';
+      };
+
       source = mkOption {
         default = "/dev/urandom";
         example = "/dev/random";
@@ -66,7 +94,7 @@ let
 
       device = mkOption {
         example = "/dev/sda3";
-        type = types.str;
+        type = types.nonEmptyStr;
         description = lib.mdDoc "Path of the device or swap file.";
       };
 
@@ -157,11 +185,11 @@ let
 
     };
 
-    config = rec {
+    config = {
       device = mkIf options.label.isDefined
         "/dev/disk/by-label/${config.label}";
       deviceName = lib.replaceStrings ["\\"] [""] (escapeSystemdPath config.device);
-      realDevice = if config.randomEncryption.enable then "/dev/mapper/${deviceName}" else config.device;
+      realDevice = if config.randomEncryption.enable then "/dev/mapper/${config.deviceName}" else config.device;
     };
 
   };
@@ -197,6 +225,21 @@ in
   };
 
   config = mkIf ((length config.swapDevices) != 0) {
+    assertions = map (sw: {
+      assertion = sw.randomEncryption.enable -> builtins.match "/dev/disk/by-(uuid|label)/.*" sw.device == null;
+      message = ''
+        You cannot use swap device "${sw.device}" with randomEncryption enabled.
+        The UUIDs and labels will get erased on every boot when the partition is encrypted.
+        Use /dev/disk/by-partuuid/… instead.
+      '';
+    }) config.swapDevices;
+
+    warnings =
+      concatMap (sw:
+        if sw.size != null && hasPrefix "/dev/" sw.device
+        then [ "Setting the swap size of block device ${sw.device} has no effect" ]
+        else [ ])
+      config.swapDevices;
 
     system.requiredKernelConfig = with config.lib.kernelConfig; [
       (isYes "SWAP")
@@ -205,31 +248,38 @@ in
     # Create missing swapfiles.
     systemd.services =
       let
-
         createSwapDevice = sw:
-          assert sw.device != "";
-          assert !(sw.randomEncryption.enable && lib.hasPrefix "/dev/disk/by-uuid"  sw.device);
-          assert !(sw.randomEncryption.enable && lib.hasPrefix "/dev/disk/by-label" sw.device);
           let realDevice' = escapeSystemdPath sw.realDevice;
           in nameValuePair "mkswap-${sw.deviceName}"
           { description = "Initialisation of swap device ${sw.device}";
             wantedBy = [ "${realDevice'}.swap" ];
             before = [ "${realDevice'}.swap" ];
-            path = [ pkgs.util-linux ] ++ optional sw.randomEncryption.enable pkgs.cryptsetup;
+            path = [ pkgs.util-linux pkgs.e2fsprogs ]
+              ++ optional sw.randomEncryption.enable pkgs.cryptsetup;
+
+            environment.DEVICE = sw.device;
 
             script =
               ''
                 ${optionalString (sw.size != null) ''
-                  currentSize=$(( $(stat -c "%s" "${sw.device}" 2>/dev/null || echo 0) / 1024 / 1024 ))
-                  if [ "${toString sw.size}" != "$currentSize" ]; then
-                    dd if=/dev/zero of="${sw.device}" bs=1M count=${toString sw.size}
+                  currentSize=$(( $(stat -c "%s" "$DEVICE" 2>/dev/null || echo 0) / 1024 / 1024 ))
+                  if [[ ! -b "$DEVICE" && "${toString sw.size}" != "$currentSize" ]]; then
+                    # Disable CoW for CoW based filesystems like BTRFS.
+                    truncate --size 0 "$DEVICE"
+                    chattr +C "$DEVICE" 2>/dev/null || true
+
+                    dd if=/dev/zero of="$DEVICE" bs=1M count=${toString sw.size}
                     chmod 0600 ${sw.device}
                     ${optionalString (!sw.randomEncryption.enable) "mkswap ${sw.realDevice}"}
                   fi
                 ''}
                 ${optionalString sw.randomEncryption.enable ''
                   cryptsetup plainOpen -c ${sw.randomEncryption.cipher} -d ${sw.randomEncryption.source} \
-                    ${optionalString sw.randomEncryption.allowDiscards "--allow-discards"} ${sw.device} ${sw.deviceName}
+                  ${concatStringsSep " \\\n" (flatten [
+                    (optional (sw.randomEncryption.sectorSize != null) "--sector-size=${toString sw.randomEncryption.sectorSize}")
+                    (optional (sw.randomEncryption.keySize != null) "--key-size=${toString sw.randomEncryption.keySize}")
+                    (optional sw.randomEncryption.allowDiscards "--allow-discards")
+                  ])} ${sw.device} ${sw.deviceName}
                   mkswap ${sw.realDevice}
                 ''}
               '';
diff --git a/nixos/modules/config/sysctl.nix b/nixos/modules/config/sysctl.nix
index 4346c88f768..0bc7ab9667f 100644
--- a/nixos/modules/config/sysctl.nix
+++ b/nixos/modules/config/sysctl.nix
@@ -72,5 +72,8 @@ in
     # Disable YAMA by default to allow easy debugging.
     boot.kernel.sysctl."kernel.yama.ptrace_scope" = mkDefault 0;
 
+    # Improve compatibility with applications that allocate
+    # a lot of memory, like modern games
+    boot.kernel.sysctl."vm.max_map_count" = mkDefault 1048576;
   };
 }
diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl
index 4368ec24ea9..54352a517a2 100644
--- a/nixos/modules/config/update-users-groups.pl
+++ b/nixos/modules/config/update-users-groups.pl
@@ -215,10 +215,12 @@ foreach my $u (@{$spec->{users}}) {
     } else {
         $u->{uid} = allocUid($name, $u->{isSystemUser}) if !defined $u->{uid};
 
-        if (defined $u->{initialPassword}) {
-            $u->{hashedPassword} = hashPassword($u->{initialPassword});
-        } elsif (defined $u->{initialHashedPassword}) {
-            $u->{hashedPassword} = $u->{initialHashedPassword};
+        if (!defined $u->{hashedPassword}) {
+            if (defined $u->{initialPassword}) {
+                $u->{hashedPassword} = hashPassword($u->{initialPassword});
+            } elsif (defined $u->{initialHashedPassword}) {
+                $u->{hashedPassword} = $u->{initialHashedPassword};
+            }
         }
     }
 
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
index 19319b9309c..4c9e286ea5f 100644
--- a/nixos/modules/config/users-groups.nix
+++ b/nixos/modules/config/users-groups.nix
@@ -90,7 +90,7 @@ let
           only has an effect if {option}`uid` is
           {option}`null`, in which case it determines whether
           the user's UID is allocated in the range for system users
-          (below 500) or in the range for normal users (starting at
+          (below 1000) or in the range for normal users (starting at
           1000).
           Exactly one of `isNormalUser` and
           `isSystemUser` must be true.
@@ -273,6 +273,9 @@ let
           {command}`passwd` command. Otherwise, it's
           equivalent to setting the {option}`hashedPassword` option.
 
+          Note that the {option}`hashedPassword` option will override
+          this option if both are set.
+
           ${hashedPasswordDescription}
         '';
       };
@@ -291,6 +294,9 @@ let
           is world-readable in the Nix store, so it should only be
           used for guest accounts or passwords that will be changed
           promptly.
+
+          Note that the {option}`password` option will override this
+          option if both are set.
         '';
       };
 
@@ -422,6 +428,8 @@ let
 
   uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid";
   gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid";
+  sdInitrdUidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) config.boot.initrd.systemd.users) "uid";
+  sdInitrdGidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) config.boot.initrd.systemd.groups) "gid";
 
   spec = pkgs.writeText "users-groups.json" (builtins.toJSON {
     inherit (cfg) mutableUsers;
@@ -444,8 +452,8 @@ let
 
 in {
   imports = [
-    (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ])
-    (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ])
+    (mkAliasOptionModuleMD [ "users" "extraUsers" ] [ "users" "users" ])
+    (mkAliasOptionModuleMD [ "users" "extraGroups" ] [ "users" "groups" ])
     (mkRenamedOptionModule ["security" "initialRootPassword"] ["users" "users" "root" "initialHashedPassword"])
   ];
 
@@ -528,12 +536,57 @@ in {
         WARNING: enabling this can lock you out of your system. Enable this only if you know what are you doing.
       '';
     };
+
+    # systemd initrd
+    boot.initrd.systemd.users = mkOption {
+      description = ''
+        Users to include in initrd.
+      '';
+      default = {};
+      type = types.attrsOf (types.submodule ({ name, ... }: {
+        options.uid = mkOption {
+          type = types.int;
+          description = ''
+            ID of the user in initrd.
+          '';
+          defaultText = literalExpression "config.users.users.\${name}.uid";
+          default = cfg.users.${name}.uid;
+        };
+        options.group = mkOption {
+          type = types.singleLineStr;
+          description = ''
+            Group the user belongs to in initrd.
+          '';
+          defaultText = literalExpression "config.users.users.\${name}.group";
+          default = cfg.users.${name}.group;
+        };
+      }));
+    };
+
+    boot.initrd.systemd.groups = mkOption {
+      description = ''
+        Groups to include in initrd.
+      '';
+      default = {};
+      type = types.attrsOf (types.submodule ({ name, ... }: {
+        options.gid = mkOption {
+          type = types.int;
+          description = ''
+            ID of the group in initrd.
+          '';
+          defaultText = literalExpression "config.users.groups.\${name}.gid";
+          default = cfg.groups.${name}.gid;
+        };
+      }));
+    };
   };
 
 
   ###### implementation
 
-  config = {
+  config = let
+    cryptSchemeIdPatternGroup = "(${lib.concatStringsSep "|" pkgs.libxcrypt.enabledCryptSchemeIds})";
+  in {
 
     users.users = {
       root = {
@@ -594,16 +647,17 @@ in {
       deps = [ "users" ];
       text = ''
         users=()
-        while IFS=: read -r user hash tail; do
-          if [[ "$hash" = "$"* && ! "$hash" =~ ^\$(y|gy|7|2b|2y|2a|6)\$ ]]; then
+        while IFS=: read -r user hash _; do
+          if [[ "$hash" = "$"* && ! "$hash" =~ ^\''$${cryptSchemeIdPatternGroup}\$ ]]; then
             users+=("$user")
           fi
         done </etc/shadow
 
         if (( "''${#users[@]}" )); then
           echo "
-        WARNING: The following user accounts rely on password hashes that will
-        be removed in NixOS 23.05. They should be renewed as soon as possible."
+        WARNING: The following user accounts rely on password hashing algorithms
+        that have been removed. They need to be renewed as soon as possible, as
+        they do prevent their users from logging in."
           printf ' - %s\n' "''${users[@]}"
         fi
       '';
@@ -630,10 +684,52 @@ in {
       "/etc/profiles/per-user/$USER"
     ];
 
+    # systemd initrd
+    boot.initrd.systemd = lib.mkIf config.boot.initrd.systemd.enable {
+      contents = {
+        "/etc/passwd".text = ''
+          ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { uid, group }: let
+            g = config.boot.initrd.systemd.groups.${group};
+          in "${n}:x:${toString uid}:${toString g.gid}::/var/empty:") config.boot.initrd.systemd.users)}
+        '';
+        "/etc/group".text = ''
+          ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { gid }: "${n}:x:${toString gid}:") config.boot.initrd.systemd.groups)}
+        '';
+      };
+
+      users = {
+        root = {};
+        nobody = {};
+      };
+
+      groups = {
+        root = {};
+        nogroup = {};
+        systemd-journal = {};
+        tty = {};
+        dialout = {};
+        kmem = {};
+        input = {};
+        video = {};
+        render = {};
+        sgx = {};
+        audio = {};
+        video = {};
+        lp = {};
+        disk = {};
+        cdrom = {};
+        tape = {};
+        kvm = {};
+      };
+    };
+
     assertions = [
       { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
         message = "UIDs and GIDs must be unique!";
       }
+      { assertion = !cfg.enforceIdUniqueness || (sdInitrdUidsAreUnique && sdInitrdGidsAreUnique);
+        message = "systemd initrd UIDs and GIDs must be unique!";
+      }
       { # If mutableUsers is false, to prevent users creating a
         # configuration that locks them out of the system, ensure that
         # there is at least one "privileged" account that has a
@@ -677,7 +773,7 @@ in {
           {
             assertion = let
               xor = a: b: a && !b || b && !a;
-              isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 500);
+              isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 1000);
             in xor isEffectivelySystemUser user.isNormalUser;
             message = ''
               Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set.
@@ -693,7 +789,20 @@ in {
               users.groups.${user.name} = {};
             '';
           }
-        ]
+        ] ++ (map (shell: {
+            assertion = (user.shell == pkgs.${shell}) -> (config.programs.${shell}.enable == true);
+            message = ''
+              users.users.${user.name}.shell is set to ${shell}, but
+              programs.${shell}.enable is not true. This will cause the ${shell}
+              shell to lack the basic nix directories in its PATH and might make
+              logging in as that user impossible. You can fix it with:
+              programs.${shell}.enable = true;
+            '';
+          }) [
+          "fish"
+          "xonsh"
+          "zsh"
+        ])
     ));
 
     warnings =
@@ -710,9 +819,10 @@ in {
         let
           sep = "\\$";
           base64 = "[a-zA-Z0-9./]+";
-          id = "[a-z0-9-]+";
+          id = cryptSchemeIdPatternGroup;
+          name = "[a-z0-9-]+";
           value = "[a-zA-Z0-9/+.-]+";
-          options = "${id}(=${value})?(,${id}=${value})*";
+          options = "${name}(=${value})?(,${name}=${value})*";
           scheme  = "${id}(${sep}${options})?";
           content = "${base64}${sep}${base64}(${sep}${base64})?";
           mcf = "^${sep}${scheme}${sep}${content}$";
diff --git a/nixos/modules/config/xdg/portal.nix b/nixos/modules/config/xdg/portal.nix
index ab6cffe499a..e19e5cf28b3 100644
--- a/nixos/modules/config/xdg/portal.nix
+++ b/nixos/modules/config/xdg/portal.nix
@@ -21,7 +21,7 @@ in
       in
       {
         warnings = lib.mkIf config.xdg.portal.gtkUsePortal [
-          "The option `${lib.showOption from}' defined in ${lib.showFiles fromOpt.files} has been deprecated. Setting the variable globally with `environment.sessionVariables' NixOS option can have unforseen side-effects."
+          "The option `${lib.showOption from}' defined in ${lib.showFiles fromOpt.files} has been deprecated. Setting the variable globally with `environment.sessionVariables' NixOS option can have unforeseen side-effects."
         ];
       }
     )
diff --git a/nixos/modules/config/zram.nix b/nixos/modules/config/zram.nix
index 87ac53a60b7..991387ea9b2 100644
--- a/nixos/modules/config/zram.nix
+++ b/nixos/modules/config/zram.nix
@@ -1,45 +1,27 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
 
   cfg = config.zramSwap;
-
-  # don't set swapDevices as mkDefault, so we can detect user had read our warning
-  # (see below) and made an action (or not)
-  devicesCount = if cfg.swapDevices != null then cfg.swapDevices else cfg.numDevices;
-
-  devices = map (nr: "zram${toString nr}") (range 0 (devicesCount - 1));
-
-  modprobe = "${pkgs.kmod}/bin/modprobe";
-
-  warnings =
-  assert cfg.swapDevices != null -> cfg.numDevices >= cfg.swapDevices;
-  flatten [
-    (optional (cfg.numDevices > 1 && cfg.swapDevices == null) ''
-      Using several small zram devices as swap is no better than using one large.
-      Set either zramSwap.numDevices = 1 or explicitly set zramSwap.swapDevices.
-
-      Previously multiple zram devices were used to enable multithreaded
-      compression. Linux supports multithreaded compression for 1 device
-      since 3.15. See https://lkml.org/lkml/2014/2/28/404 for details.
-    '')
-  ];
+  devices = map (nr: "zram${toString nr}") (lib.range 0 (cfg.swapDevices - 1));
 
 in
 
 {
 
+  imports = [
+    (lib.mkRemovedOptionModule [ "zramSwap" "numDevices" ] "Using ZRAM devices as general purpose ephemeral block devices is no longer supported")
+  ];
+
   ###### interface
 
   options = {
 
     zramSwap = {
 
-      enable = mkOption {
+      enable = lib.mkOption {
         default = false;
-        type = types.bool;
+        type = lib.types.bool;
         description = lib.mdDoc ''
           Enable in-memory compressed devices and swap space provided by the zram
           kernel module.
@@ -49,29 +31,17 @@ in
         '';
       };
 
-      numDevices = mkOption {
+      swapDevices = lib.mkOption {
         default = 1;
-        type = types.int;
+        type = lib.types.int;
         description = lib.mdDoc ''
-          Number of zram devices to create. See also
-          `zramSwap.swapDevices`
+          Number of zram devices to be used as swap, recommended is 1.
         '';
       };
 
-      swapDevices = mkOption {
-        default = null;
-        example = 1;
-        type = with types; nullOr int;
-        description = lib.mdDoc ''
-          Number of zram devices to be used as swap. Must be
-          `<= zramSwap.numDevices`.
-          Default is same as `zramSwap.numDevices`, recommended is 1.
-        '';
-      };
-
-      memoryPercent = mkOption {
+      memoryPercent = lib.mkOption {
         default = 50;
-        type = types.int;
+        type = lib.types.int;
         description = lib.mdDoc ''
           Maximum total amount of memory that can be stored in the zram swap devices
           (as a percentage of your total memory). Defaults to 1/2 of your total
@@ -80,9 +50,9 @@ in
         '';
       };
 
-      memoryMax = mkOption {
+      memoryMax = lib.mkOption {
         default = null;
-        type = with types; nullOr int;
+        type = with lib.types; nullOr int;
         description = lib.mdDoc ''
           Maximum total amount of memory (in bytes) that can be stored in the zram
           swap devices.
@@ -90,9 +60,9 @@ in
         '';
       };
 
-      priority = mkOption {
+      priority = lib.mkOption {
         default = 5;
-        type = types.int;
+        type = lib.types.int;
         description = lib.mdDoc ''
           Priority of the zram swap devices. It should be a number higher than
           the priority of your disk-based swap devices (so that the system will
@@ -100,10 +70,10 @@ in
         '';
       };
 
-      algorithm = mkOption {
+      algorithm = lib.mkOption {
         default = "zstd";
         example = "lz4";
-        type = with types; either (enum [ "lzo" "lz4" "zstd" ]) str;
+        type = with lib.types; either (enum [ "lzo" "lz4" "zstd" ]) str;
         description = lib.mdDoc ''
           Compression algorithm. `lzo` has good compression,
           but is slow. `lz4` has bad compression, but is fast.
@@ -112,13 +82,29 @@ in
           {command}`cat /sys/class/block/zram*/comp_algorithm`
         '';
       };
+
+      writebackDevice = lib.mkOption {
+        default = null;
+        example = "/dev/zvol/tarta-zoot/swap-writeback";
+        type = lib.types.nullOr lib.types.path;
+        description = lib.mdDoc ''
+          Write incompressible pages to this device,
+          as there's no gain from keeping them in RAM.
+        '';
+      };
     };
 
   };
 
-  config = mkIf cfg.enable {
+  config = lib.mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = cfg.writebackDevice == null || cfg.swapDevices <= 1;
+        message = "A single writeback device cannot be shared among multiple zram devices";
+      }
+    ];
 
-    inherit warnings;
 
     system.requiredKernelConfig = with config.lib.kernelConfig; [
       (isModule "ZRAM")
@@ -128,78 +114,27 @@ in
     # once in stage 2 boot, and again when the zram-reloader service starts.
     # boot.kernelModules = [ "zram" ];
 
-    boot.extraModprobeConfig = ''
-      options zram num_devices=${toString cfg.numDevices}
-    '';
-
-    boot.kernelParams = ["zram.num_devices=${toString cfg.numDevices}"];
-
-    services.udev.extraRules = ''
-      KERNEL=="zram[0-9]*", ENV{SYSTEMD_WANTS}="zram-init-%k.service", TAG+="systemd"
-    '';
-
-    systemd.services =
-      let
-        createZramInitService = dev:
-          nameValuePair "zram-init-${dev}" {
-            description = "Init swap on zram-based device ${dev}";
-            after = [ "dev-${dev}.device" "zram-reloader.service" ];
-            requires = [ "dev-${dev}.device" "zram-reloader.service" ];
-            before = [ "dev-${dev}.swap" ];
-            requiredBy = [ "dev-${dev}.swap" ];
-            unitConfig.DefaultDependencies = false; # needed to prevent a cycle
-            serviceConfig = {
-              Type = "oneshot";
-              RemainAfterExit = true;
-              ExecStop = "${pkgs.runtimeShell} -c 'echo 1 > /sys/class/block/${dev}/reset'";
-            };
-            script = ''
-              set -euo pipefail
-
-              # Calculate memory to use for zram
-              mem=$(${pkgs.gawk}/bin/awk '/MemTotal: / {
-                  value=int($2*${toString cfg.memoryPercent}/100.0/${toString devicesCount}*1024);
-                    ${lib.optionalString (cfg.memoryMax != null) ''
-                      memory_max=int(${toString cfg.memoryMax}/${toString devicesCount});
-                      if (value > memory_max) { value = memory_max }
-                    ''}
-                  print value
-              }' /proc/meminfo)
-
-              ${pkgs.util-linux}/sbin/zramctl --size $mem --algorithm ${cfg.algorithm} /dev/${dev}
-              ${pkgs.util-linux}/sbin/mkswap /dev/${dev}
-            '';
-            restartIfChanged = false;
-          };
-      in listToAttrs ((map createZramInitService devices) ++ [(nameValuePair "zram-reloader"
-        {
-          description = "Reload zram kernel module when number of devices changes";
-          wants = [ "systemd-udevd.service" ];
-          after = [ "systemd-udevd.service" ];
-          unitConfig.DefaultDependencies = false; # needed to prevent a cycle
-          serviceConfig = {
-            Type = "oneshot";
-            RemainAfterExit = true;
-            ExecStartPre = "-${modprobe} -r zram";
-            ExecStart = "-${modprobe} zram";
-            ExecStop = "-${modprobe} -r zram";
-          };
-          restartTriggers = [
-            cfg.numDevices
-            cfg.algorithm
-            cfg.memoryPercent
-          ];
-          restartIfChanged = true;
-        })]);
-
-    swapDevices =
-      let
-        useZramSwap = dev:
-          {
-            device = "/dev/${dev}";
-            priority = cfg.priority;
-          };
-      in map useZramSwap devices;
+    systemd.packages = [ pkgs.zram-generator ];
+    systemd.services."systemd-zram-setup@".path = [ pkgs.util-linux ]; # for mkswap
+
+    environment.etc."systemd/zram-generator.conf".source =
+      (pkgs.formats.ini { }).generate "zram-generator.conf" (lib.listToAttrs
+        (builtins.map
+          (dev: {
+            name = dev;
+            value =
+              let
+                size = "${toString cfg.memoryPercent} / 100 * ram";
+              in
+              {
+                zram-size = if cfg.memoryMax != null then "min(${size}, ${toString cfg.memoryMax} / 1024 / 1024)" else size;
+                compression-algorithm = cfg.algorithm;
+                swap-priority = cfg.priority;
+              } // lib.optionalAttrs (cfg.writebackDevice != null) {
+                writeback-device = cfg.writebackDevice;
+              };
+          })
+          devices));
 
   };
 
diff --git a/nixos/modules/hardware/all-firmware.nix b/nixos/modules/hardware/all-firmware.nix
index 2d5a0007ff0..9e7a01c58af 100644
--- a/nixos/modules/hardware/all-firmware.nix
+++ b/nixos/modules/hardware/all-firmware.nix
@@ -55,7 +55,6 @@ in {
         intel2200BGFirmware
         rtl8192su-firmware
         rt5677-firmware
-        rtl8723bs-firmware
         rtl8761b-firmware
         rtw88-firmware
         zd1211fw
@@ -65,8 +64,6 @@ in {
       ] ++ optional pkgs.stdenv.hostPlatform.isAarch raspberrypiWirelessFirmware
         ++ optionals (versionOlder config.boot.kernelPackages.kernel.version "4.13") [
         rtl8723bs-firmware
-      ] ++ optionals (versionOlder config.boot.kernelPackages.kernel.version "5.16") [
-        rtw89-firmware
       ];
       hardware.wirelessRegulatoryDatabase = true;
     })
diff --git a/nixos/modules/hardware/device-tree.nix b/nixos/modules/hardware/device-tree.nix
index 2807313a5a9..c568f52ab67 100644
--- a/nixos/modules/hardware/device-tree.nix
+++ b/nixos/modules/hardware/device-tree.nix
@@ -65,7 +65,7 @@ let
     };
   };
 
-  filterDTBs = src: if isNull cfg.filter
+  filterDTBs = src: if cfg.filter == null
     then "${src}/dtbs"
     else
       pkgs.runCommand "dtbs-filtered" {} ''
@@ -93,8 +93,8 @@ let
   # Fill in `dtboFile` for each overlay if not set already.
   # Existence of one of these is guarded by assertion below
   withDTBOs = xs: flip map xs (o: o // { dtboFile =
-    if isNull o.dtboFile then
-      if !isNull o.dtsFile then compileDTS o.name o.dtsFile
+    if o.dtboFile == null then
+      if o.dtsFile != null then compileDTS o.name o.dtsFile
       else compileDTS o.name (pkgs.writeText "dts" o.dtsText)
     else o.dtboFile; } );
 
@@ -181,7 +181,7 @@ in
   config = mkIf (cfg.enable) {
 
     assertions = let
-      invalidOverlay = o: isNull o.dtsFile && isNull o.dtsText && isNull o.dtboFile;
+      invalidOverlay = o: (o.dtsFile == null) && (o.dtsText == null) && (o.dtboFile == null);
     in lib.singleton {
       assertion = lib.all (o: !invalidOverlay o) cfg.overlays;
       message = ''
diff --git a/nixos/modules/hardware/flipperzero.nix b/nixos/modules/hardware/flipperzero.nix
new file mode 100644
index 00000000000..82f9b76fa3a
--- /dev/null
+++ b/nixos/modules/hardware/flipperzero.nix
@@ -0,0 +1,18 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.hardware.flipperzero;
+
+in
+
+{
+  options.hardware.flipperzero.enable = mkEnableOption (mdDoc "udev rules and software for Flipper Zero devices");
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.qFlipper ];
+    services.udev.packages = [ pkgs.qFlipper ];
+  };
+}
diff --git a/nixos/modules/hardware/i2c.nix b/nixos/modules/hardware/i2c.nix
index c0423cc5d99..9a5a2e44813 100644
--- a/nixos/modules/hardware/i2c.nix
+++ b/nixos/modules/hardware/i2c.nix
@@ -1,4 +1,4 @@
-{ config, lib, ... }:
+{ config, lib, pkgs, ... }:
 
 with lib;
 
@@ -31,10 +31,14 @@ in
       i2c = { };
     };
 
-    services.udev.extraRules = ''
-      # allow group ${cfg.group} and users with a seat use of i2c devices
-      ACTION=="add", KERNEL=="i2c-[0-9]*", TAG+="uaccess", GROUP="${cfg.group}", MODE="660"
-    '';
+    services.udev.packages = lib.singleton (pkgs.writeTextFile
+      { name = "i2c-udev-rules";
+        text = ''
+          # allow group ${cfg.group} and users with a seat use of i2c devices
+          ACTION=="add", KERNEL=="i2c-[0-9]*", TAG+="uaccess", GROUP="${cfg.group}", MODE="660"
+        '';
+        destination = "/etc/udev/rules.d/70-i2c.rules";
+      });
 
   };
 
diff --git a/nixos/modules/hardware/keyboard/qmk.nix b/nixos/modules/hardware/keyboard/qmk.nix
new file mode 100644
index 00000000000..df3bcaeccd2
--- /dev/null
+++ b/nixos/modules/hardware/keyboard/qmk.nix
@@ -0,0 +1,16 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.hardware.keyboard.qmk;
+  inherit (lib) mdDoc mkEnableOption mkIf;
+
+in
+{
+  options.hardware.keyboard.qmk = {
+    enable = mkEnableOption (mdDoc "non-root access to the firmware of QMK keyboards");
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.qmk-udev-rules ];
+  };
+}
diff --git a/nixos/modules/hardware/keyboard/teck.nix b/nixos/modules/hardware/keyboard/teck.nix
index 2705668d9a7..8376c6b9c50 100644
--- a/nixos/modules/hardware/keyboard/teck.nix
+++ b/nixos/modules/hardware/keyboard/teck.nix
@@ -1,16 +1,16 @@
 { config, lib, pkgs, ... }:
 
-with lib;
 let
   cfg = config.hardware.keyboard.teck;
+  inherit (lib) mdDoc mkEnableOption mkIf;
+
 in
 {
   options.hardware.keyboard.teck = {
-    enable = mkEnableOption (lib.mdDoc "non-root access to the firmware of TECK keyboards");
+    enable = mkEnableOption (mdDoc "non-root access to the firmware of TECK keyboards");
   };
 
   config = mkIf cfg.enable {
     services.udev.packages = [ pkgs.teck-udev-rules ];
   };
 }
-
diff --git a/nixos/modules/hardware/keyboard/uhk.nix b/nixos/modules/hardware/keyboard/uhk.nix
index c1805143993..17baff83d88 100644
--- a/nixos/modules/hardware/keyboard/uhk.nix
+++ b/nixos/modules/hardware/keyboard/uhk.nix
@@ -1,13 +1,14 @@
 { config, lib, pkgs, ... }:
 
-with lib;
 let
   cfg = config.hardware.keyboard.uhk;
+  inherit (lib) mdDoc mkEnableOption mkIf;
+
 in
 {
   options.hardware.keyboard.uhk = {
-    enable = mkEnableOption (lib.mdDoc ''
-    non-root access to the firmware of UHK keyboards.
+    enable = mkEnableOption (mdDoc ''
+      non-root access to the firmware of UHK keyboards.
       You need it when you want to flash a new firmware on the keyboard.
       Access to the keyboard is granted to users in the "input" group.
       You may want to install the uhk-agent package.
diff --git a/nixos/modules/hardware/keyboard/zsa.nix b/nixos/modules/hardware/keyboard/zsa.nix
index 5bf4022cdc4..a04b67b5c8d 100644
--- a/nixos/modules/hardware/keyboard/zsa.nix
+++ b/nixos/modules/hardware/keyboard/zsa.nix
@@ -1,21 +1,18 @@
 { config, lib, pkgs, ... }:
 
 let
-  inherit (lib) mkOption mkIf types;
   cfg = config.hardware.keyboard.zsa;
+  inherit (lib) mkEnableOption mkIf mdDoc;
+
 in
 {
   options.hardware.keyboard.zsa = {
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Enables udev rules for keyboards from ZSA like the ErgoDox EZ, Planck EZ and Moonlander Mark I.
-        You need it when you want to flash a new configuration on the keyboard
-        or use their live training in the browser.
-        You may want to install the wally-cli package.
-      '';
-    };
+    enable = mkEnableOption (mdDoc ''
+      udev rules for keyboards from ZSA like the ErgoDox EZ, Planck EZ and Moonlander Mark I.
+      You need it when you want to flash a new configuration on the keyboard
+      or use their live training in the browser.
+      You may want to install the wally-cli package.
+    '');
   };
 
   config = mkIf cfg.enable {
diff --git a/nixos/modules/hardware/nitrokey.nix b/nixos/modules/hardware/nitrokey.nix
index fa9dd4d6d8f..e2e88a8eade 100644
--- a/nixos/modules/hardware/nitrokey.nix
+++ b/nixos/modules/hardware/nitrokey.nix
@@ -22,6 +22,6 @@ in
   };
 
   config = mkIf cfg.enable {
-    services.udev.packages = [ pkgs.nitrokey-udev-rules ];
+    services.udev.packages = [ pkgs.libnitrokey ];
   };
 }
diff --git a/nixos/modules/hardware/opengl.nix b/nixos/modules/hardware/opengl.nix
index 9108bcbd165..0ff018ddc47 100644
--- a/nixos/modules/hardware/opengl.nix
+++ b/nixos/modules/hardware/opengl.nix
@@ -87,13 +87,13 @@ in
       extraPackages = mkOption {
         type = types.listOf types.package;
         default = [];
-        example = literalExpression "with pkgs; [ intel-media-driver intel-ocl vaapiIntel ]";
+        example = literalExpression "with pkgs; [ intel-media-driver intel-ocl intel-vaapi-driver ]";
         description = lib.mdDoc ''
           Additional packages to add to OpenGL drivers.
           This can be used to add OpenCL drivers, VA-API/VDPAU drivers etc.
 
           ::: {.note}
-          intel-media-driver supports hardware Broadwell (2014) or newer. Older hardware should use the mostly unmaintained vaapiIntel driver.
+          intel-media-driver supports hardware Broadwell (2014) or newer. Older hardware should use the mostly unmaintained intel-vaapi-driver driver.
           :::
         '';
       };
@@ -101,13 +101,13 @@ in
       extraPackages32 = mkOption {
         type = types.listOf types.package;
         default = [];
-        example = literalExpression "with pkgs.pkgsi686Linux; [ intel-media-driver vaapiIntel ]";
+        example = literalExpression "with pkgs.pkgsi686Linux; [ intel-media-driver intel-vaapi-driver ]";
         description = lib.mdDoc ''
           Additional packages to add to 32-bit OpenGL drivers on 64-bit systems.
           Used when {option}`driSupport32Bit` is set. This can be used to add OpenCL drivers, VA-API/VDPAU drivers etc.
 
           ::: {.note}
-          intel-media-driver supports hardware Broadwell (2014) or newer. Older hardware should use the mostly unmaintained vaapiIntel driver.
+          intel-media-driver supports hardware Broadwell (2014) or newer. Older hardware should use the mostly unmaintained intel-vaapi-driver driver.
           :::
         '';
       };
diff --git a/nixos/modules/hardware/opentabletdriver.nix b/nixos/modules/hardware/opentabletdriver.nix
index 6c5ca3d949e..e3f418abce4 100644
--- a/nixos/modules/hardware/opentabletdriver.nix
+++ b/nixos/modules/hardware/opentabletdriver.nix
@@ -61,7 +61,7 @@ in
 
       serviceConfig = {
         Type = "simple";
-        ExecStart = "${cfg.package}/bin/otd-daemon -c ${cfg.package}/lib/OpenTabletDriver/Configurations";
+        ExecStart = "${cfg.package}/bin/otd-daemon";
         Restart = "on-failure";
       };
     };
diff --git a/nixos/modules/hardware/printers.nix b/nixos/modules/hardware/printers.nix
index 85e3215127f..846ff6f3fb4 100644
--- a/nixos/modules/hardware/printers.nix
+++ b/nixos/modules/hardware/printers.nix
@@ -110,21 +110,26 @@ in {
   };
 
   config = mkIf (cfg.ensurePrinters != [] && config.services.printing.enable) {
-    systemd.services.ensure-printers = let
-      cupsUnit = if config.services.printing.startWhenNeeded then "cups.socket" else "cups.service";
-    in {
+    systemd.services.ensure-printers = {
       description = "Ensure NixOS-configured CUPS printers";
       wantedBy = [ "multi-user.target" ];
-      requires = [ cupsUnit ];
-      after = [ cupsUnit ];
+      wants = [ "cups.service" ];
+      after = [ "cups.service" ];
 
       serviceConfig = {
         Type = "oneshot";
         RemainAfterExit = true;
       };
 
-      script = concatMapStringsSep "\n" ensurePrinter cfg.ensurePrinters
-        + optionalString (cfg.ensureDefaultPrinter != null) (ensureDefaultPrinter cfg.ensureDefaultPrinter);
+      script = concatStringsSep "\n" [
+        (concatMapStrings ensurePrinter cfg.ensurePrinters)
+        (optionalString (cfg.ensureDefaultPrinter != null)
+          (ensureDefaultPrinter cfg.ensureDefaultPrinter))
+        # Note: if cupsd is "stateless" the service can't be stopped,
+        # otherwise the configuration will be wiped on the next start.
+        (optionalString (with config.services.printing; startWhenNeeded && !stateless)
+          "systemctl stop cups.service")
+      ];
     };
   };
 }
diff --git a/nixos/modules/hardware/sensor/hddtemp.nix b/nixos/modules/hardware/sensor/hddtemp.nix
index b69d012b4d0..1a3d211b858 100644
--- a/nixos/modules/hardware/sensor/hddtemp.nix
+++ b/nixos/modules/hardware/sensor/hddtemp.nix
@@ -43,7 +43,7 @@ in
       };
 
       unit = mkOption {
-        description = lib.mdDoc "Celcius or Fahrenheit";
+        description = lib.mdDoc "Celsius or Fahrenheit";
         type = types.enum [ "C" "F" ];
         default = "C";
       };
diff --git a/nixos/modules/hardware/video/displaylink.nix b/nixos/modules/hardware/video/displaylink.nix
index 912f53da836..ce5fbeeae53 100644
--- a/nixos/modules/hardware/video/displaylink.nix
+++ b/nixos/modules/hardware/video/displaylink.nix
@@ -26,6 +26,7 @@ in
         Identifier  "DisplayLink"
         MatchDriver "evdi"
         Driver      "modesetting"
+        Option      "TearFree" "true"
         Option      "AccelMethod" "none"
       EndSection
     '';
diff --git a/nixos/modules/hardware/video/hidpi.nix b/nixos/modules/hardware/video/hidpi.nix
deleted file mode 100644
index 8c8f8bc0c26..00000000000
--- a/nixos/modules/hardware/video/hidpi.nix
+++ /dev/null
@@ -1,24 +0,0 @@
-{ lib, pkgs, config, ...}:
-with lib;
-
-{
-  options.hardware.video.hidpi.enable = mkEnableOption (lib.mdDoc "Font/DPI configuration optimized for HiDPI displays");
-
-  config = mkIf config.hardware.video.hidpi.enable {
-    console.font = lib.mkDefault "${pkgs.terminus_font}/share/consolefonts/ter-v32n.psf.gz";
-
-    # Needed when typing in passwords for full disk encryption
-    console.earlySetup = mkDefault true;
-    boot.loader.systemd-boot.consoleMode = mkDefault "1";
-
-
-    # Grayscale anti-aliasing for fonts
-    fonts.fontconfig.antialias = mkDefault true;
-    fonts.fontconfig.subpixel = {
-      rgba = mkDefault "none";
-      lcdfilter = mkDefault "none";
-    };
-
-    # TODO Find reasonable defaults X11 & wayland
-  };
-}
diff --git a/nixos/modules/hardware/video/nvidia.nix b/nixos/modules/hardware/video/nvidia.nix
index cee230ac41c..e72194653f3 100644
--- a/nixos/modules/hardware/video/nvidia.nix
+++ b/nixos/modules/hardware/video/nvidia.nix
@@ -21,17 +21,21 @@ let
   pCfg = cfg.prime;
   syncCfg = pCfg.sync;
   offloadCfg = pCfg.offload;
-  primeEnabled = syncCfg.enable || offloadCfg.enable;
+  reverseSyncCfg = pCfg.reverseSync;
+  primeEnabled = syncCfg.enable || reverseSyncCfg.enable || offloadCfg.enable;
   nvidiaPersistencedEnabled =  cfg.nvidiaPersistenced;
   nvidiaSettings = cfg.nvidiaSettings;
   busIDType = types.strMatching "([[:print:]]+[\:\@][0-9]{1,3}\:[0-9]{1,2}\:[0-9])?";
+
+  ibtSupport = cfg.open || (nvidia_x11.ibtSupport or false);
 in
 
 {
   imports =
     [
       (mkRenamedOptionModule [ "hardware" "nvidia" "optimus_prime" "enable" ] [ "hardware" "nvidia" "prime" "sync" "enable" ])
-      (mkRenamedOptionModule [ "hardware" "nvidia" "optimus_prime" "allowExternalGpu" ] [ "hardware" "nvidia" "prime" "sync" "allowExternalGpu" ])
+      (mkRenamedOptionModule [ "hardware" "nvidia" "optimus_prime" "allowExternalGpu" ] [ "hardware" "nvidia" "prime" "allowExternalGpu" ])
+      (mkRenamedOptionModule [ "hardware" "nvidia" "prime" "sync" "allowExternalGpu" ] [ "hardware" "nvidia" "prime" "allowExternalGpu" ])
       (mkRenamedOptionModule [ "hardware" "nvidia" "optimus_prime" "nvidiaBusId" ] [ "hardware" "nvidia" "prime" "nvidiaBusId" ])
       (mkRenamedOptionModule [ "hardware" "nvidia" "optimus_prime" "intelBusId" ] [ "hardware" "nvidia" "prime" "intelBusId" ])
     ];
@@ -104,16 +108,17 @@ in
       description = lib.mdDoc ''
         Enable NVIDIA Optimus support using the NVIDIA proprietary driver via PRIME.
         If enabled, the NVIDIA GPU will be always on and used for all rendering,
-        while enabling output to displays attached only to the integrated Intel GPU
-        without a multiplexer.
+        while enabling output to displays attached only to the integrated Intel/AMD
+        GPU without a multiplexer.
 
         Note that this option only has any effect if the "nvidia" driver is specified
         in {option}`services.xserver.videoDrivers`, and it should preferably
         be the only driver there.
 
-        If this is enabled, then the bus IDs of the NVIDIA and Intel GPUs have to be
-        specified ({option}`hardware.nvidia.prime.nvidiaBusId` and
-        {option}`hardware.nvidia.prime.intelBusId`).
+        If this is enabled, then the bus IDs of the NVIDIA and Intel/AMD GPUs have to
+        be specified ({option}`hardware.nvidia.prime.nvidiaBusId` and
+        {option}`hardware.nvidia.prime.intelBusId` or
+        {option}`hardware.nvidia.prime.amdgpuBusId`).
 
         If you enable this, you may want to also enable kernel modesetting for the
         NVIDIA driver ({option}`hardware.nvidia.modesetting.enable`) in order
@@ -125,11 +130,11 @@ in
       '';
     };
 
-    hardware.nvidia.prime.sync.allowExternalGpu = mkOption {
+    hardware.nvidia.prime.allowExternalGpu = mkOption {
       type = types.bool;
       default = false;
       description = lib.mdDoc ''
-        Configure X to allow external NVIDIA GPUs when using optimus.
+        Configure X to allow external NVIDIA GPUs when using Prime [Reverse] sync optimus.
       '';
     };
 
@@ -139,9 +144,54 @@ in
       description = lib.mdDoc ''
         Enable render offload support using the NVIDIA proprietary driver via PRIME.
 
-        If this is enabled, then the bus IDs of the NVIDIA and Intel GPUs have to be
-        specified ({option}`hardware.nvidia.prime.nvidiaBusId` and
-        {option}`hardware.nvidia.prime.intelBusId`).
+        If this is enabled, then the bus IDs of the NVIDIA and Intel/AMD GPUs have to
+        be specified ({option}`hardware.nvidia.prime.nvidiaBusId` and
+        {option}`hardware.nvidia.prime.intelBusId` or
+        {option}`hardware.nvidia.prime.amdgpuBusId`).
+      '';
+    };
+
+    hardware.nvidia.prime.offload.enableOffloadCmd = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Adds a `nvidia-offload` convenience script to {option}`environment.systemPackages`
+        for offloading programs to an nvidia device. To work, should have also enabled
+        {option}`hardware.nvidia.prime.offload.enable` or {option}`hardware.nvidia.prime.reverseSync.enable`.
+
+        Example usage `nvidia-offload sauerbraten_client`.
+      '';
+    };
+
+    hardware.nvidia.prime.reverseSync.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Warning: This feature is relatively new, depending on your system this might
+        work poorly. AMD support, especially so.
+        See: https://forums.developer.nvidia.com/t/the-all-new-outputsink-feature-aka-reverse-prime/129828
+
+        Enable NVIDIA Optimus support using the NVIDIA proprietary driver via reverse
+        PRIME. If enabled, the Intel/AMD GPU will be used for all rendering, while
+        enabling output to displays attached only to the NVIDIA GPU without a
+        multiplexer.
+
+        Note that this option only has any effect if the "nvidia" driver is specified
+        in {option}`services.xserver.videoDrivers`, and it should preferably
+        be the only driver there.
+
+        If this is enabled, then the bus IDs of the NVIDIA and Intel/AMD GPUs have to
+        be specified ({option}`hardware.nvidia.prime.nvidiaBusId` and
+        {option}`hardware.nvidia.prime.intelBusId` or
+        {option}`hardware.nvidia.prime.amdgpuBusId`).
+
+        If you enable this, you may want to also enable kernel modesetting for the
+        NVIDIA driver ({option}`hardware.nvidia.modesetting.enable`) in order
+        to prevent tearing.
+
+        Note that this configuration will only be successful when a display manager
+        for which the {option}`services.xserver.displayManager.setupCommands`
+        option is supported is used.
       '';
     };
 
@@ -206,9 +256,16 @@ in
       }
 
       {
+        assertion = offloadCfg.enableOffloadCmd -> offloadCfg.enable || reverseSyncCfg.enable;
+        message = ''
+          Offload command requires offloading or reverse prime sync to be enabled.
+        '';
+      }
+
+      {
         assertion = primeEnabled -> pCfg.nvidiaBusId != "" && (pCfg.intelBusId != "" || pCfg.amdgpuBusId != "");
         message = ''
-          When NVIDIA PRIME is enabled, the GPU bus IDs must configured.
+          When NVIDIA PRIME is enabled, the GPU bus IDs must be configured.
         '';
       }
 
@@ -218,8 +275,18 @@ in
       }
 
       {
+        assertion = (reverseSyncCfg.enable && pCfg.amdgpuBusId != "") -> versionAtLeast nvidia_x11.version "470.0";
+        message = "NVIDIA PRIME render offload for AMD APUs is currently only supported on versions >= 470 beta.";
+      }
+
+      {
         assertion = !(syncCfg.enable && offloadCfg.enable);
-        message = "Only one NVIDIA PRIME solution may be used at a time.";
+        message = "PRIME Sync and Offload cannot be both enabled";
+      }
+
+      {
+        assertion = !(syncCfg.enable && reverseSyncCfg.enable);
+        message = "PRIME Sync and PRIME Reverse Sync cannot be both enabled";
       }
 
       {
@@ -257,8 +324,10 @@ in
     # - Configure the display manager to run specific `xrandr` commands which will
     #   configure/enable displays connected to the Intel iGPU / AMD APU.
 
-    services.xserver.drivers = let
-    in optional primeEnabled {
+    # reverse sync implies offloading
+    hardware.nvidia.prime.offload.enable = mkDefault reverseSyncCfg.enable;
+
+    services.xserver.drivers = optional primeEnabled {
       name = igpuDriver;
       display = offloadCfg.enable;
       modules = optionals (igpuDriver == "amdgpu") [ pkgs.xorg.xf86videoamdgpu ];
@@ -273,7 +342,7 @@ in
       deviceSection = optionalString primeEnabled
         ''
           BusID "${pCfg.nvidiaBusId}"
-          ${optionalString syncCfg.allowExternalGpu "Option \"AllowExternalGpus\""}
+          ${optionalString pCfg.allowExternalGpu "Option \"AllowExternalGpus\""}
         '';
       screenSection =
         ''
@@ -290,19 +359,22 @@ in
 
     services.xserver.serverLayoutSection = optionalString syncCfg.enable ''
       Inactive "Device-${igpuDriver}[0]"
+    '' + optionalString reverseSyncCfg.enable ''
+      Inactive "Device-nvidia[0]"
     '' + optionalString offloadCfg.enable ''
       Option "AllowNVIDIAGPUScreens"
     '';
 
     services.xserver.displayManager.setupCommands = let
-      sinkGpuProviderName = if igpuDriver == "amdgpu" then
+      gpuProviderName = if igpuDriver == "amdgpu" then
         # find the name of the provider if amdgpu
         "`${pkgs.xorg.xrandr}/bin/xrandr --listproviders | ${pkgs.gnugrep}/bin/grep -i AMD | ${pkgs.gnused}/bin/sed -n 's/^.*name://p'`"
       else
         igpuDriver;
-    in optionalString syncCfg.enable ''
+      providerCmdParams = if syncCfg.enable then "\"${gpuProviderName}\" NVIDIA-0" else "NVIDIA-G0 \"${gpuProviderName}\"";
+    in optionalString (syncCfg.enable || reverseSyncCfg.enable) ''
       # Added by nvidia configuration module for Optimus/PRIME.
-      ${pkgs.xorg.xrandr}/bin/xrandr --setprovideroutputsource "${sinkGpuProviderName}" NVIDIA-0
+      ${pkgs.xorg.xrandr}/bin/xrandr --setprovideroutputsource ${providerCmdParams}
       ${pkgs.xorg.xrandr}/bin/xrandr --auto
     '';
 
@@ -325,7 +397,16 @@ in
 
     environment.systemPackages = [ nvidia_x11.bin ]
       ++ optionals cfg.nvidiaSettings [ nvidia_x11.settings ]
-      ++ optionals nvidiaPersistencedEnabled [ nvidia_x11.persistenced ];
+      ++ optionals nvidiaPersistencedEnabled [ nvidia_x11.persistenced ]
+      ++ optionals offloadCfg.enableOffloadCmd [
+        (pkgs.writeShellScriptBin "nvidia-offload" ''
+          export __NV_PRIME_RENDER_OFFLOAD=1
+          export __NV_PRIME_RENDER_OFFLOAD_PROVIDER=NVIDIA-G0
+          export __GLX_VENDOR_LIBRARY_NAME=nvidia
+          export __VK_LAYER_NV_optimus=NVIDIA_only
+          exec "$@"
+        '')
+      ];
 
     systemd.packages = optional cfg.powerManagement.enable nvidia_x11.out;
 
@@ -382,7 +463,8 @@ in
     # If requested enable modesetting via kernel parameter.
     boot.kernelParams = optional (offloadCfg.enable || cfg.modesetting.enable) "nvidia-drm.modeset=1"
       ++ optional cfg.powerManagement.enable "nvidia.NVreg_PreserveVideoMemoryAllocations=1"
-      ++ optional cfg.open "nvidia.NVreg_OpenRmEnableUnsupportedGpus=1";
+      ++ optional cfg.open "nvidia.NVreg_OpenRmEnableUnsupportedGpus=1"
+      ++ optional (config.boot.kernelPackages.kernel.kernelAtLeast "6.2" && !ibtSupport) "ibt=off";
 
     services.udev.extraRules =
       ''
diff --git a/nixos/modules/hardware/video/webcam/ipu6.nix b/nixos/modules/hardware/video/webcam/ipu6.nix
new file mode 100644
index 00000000000..fce78cda34c
--- /dev/null
+++ b/nixos/modules/hardware/video/webcam/ipu6.nix
@@ -0,0 +1,57 @@
+{ config, lib, pkgs, ... }:
+let
+
+  inherit (lib) mkDefault mkEnableOption mkIf mkOption optional types;
+
+  cfg = config.hardware.ipu6;
+
+in
+{
+
+  options.hardware.ipu6 = {
+
+    enable = mkEnableOption (lib.mdDoc "support for Intel IPU6/MIPI cameras");
+
+    platform = mkOption {
+      type = types.enum [ "ipu6" "ipu6ep" ];
+      description = lib.mdDoc ''
+        Choose the version for your hardware platform.
+
+        Use `ipu6` for Tiger Lake and `ipu6ep` for Alder Lake respectively.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    boot.extraModulePackages = with config.boot.kernelPackages; [
+      ipu6-drivers
+    ];
+
+    hardware.firmware = with pkgs; [ ]
+      ++ optional (cfg.platform == "ipu6") ipu6-camera-bin
+      ++ optional (cfg.platform == "ipu6ep") ipu6ep-camera-bin;
+
+    services.udev.extraRules = ''
+      SUBSYSTEM=="intel-ipu6-psys", MODE="0660", GROUP="video"
+    '';
+
+    services.v4l2-relayd.instances.ipu6 = {
+      enable = mkDefault true;
+
+      cardLabel = mkDefault "Intel MIPI Camera";
+
+      extraPackages = with pkgs.gst_all_1; [ ]
+        ++ optional (cfg.platform == "ipu6") icamerasrc-ipu6
+        ++ optional (cfg.platform == "ipu6ep") icamerasrc-ipu6ep;
+
+      input = {
+        pipeline = "icamerasrc";
+        format = mkIf (cfg.platform == "ipu6ep") (mkDefault "NV12");
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/i18n/input-method/default.md b/nixos/modules/i18n/input-method/default.md
new file mode 100644
index 00000000000..42cb8a8d7b6
--- /dev/null
+++ b/nixos/modules/i18n/input-method/default.md
@@ -0,0 +1,160 @@
+# Input Methods {#module-services-input-methods}
+
+Input methods are an operating system component that allows any data, such as
+keyboard strokes or mouse movements, to be received as input. In this way
+users can enter characters and symbols not found on their input devices.
+Using an input method is obligatory for any language that has more graphemes
+than there are keys on the keyboard.
+
+The following input methods are available in NixOS:
+
+  - IBus: The intelligent input bus.
+  - Fcitx5: The next generation of fcitx, addons (including engines, dictionaries, skins) can be added using `i18n.inputMethod.fcitx5.addons`.
+  - Nabi: A Korean input method based on XIM.
+  - Uim: The universal input method, is a library with a XIM bridge.
+  - Hime: An extremely easy-to-use input method framework.
+  - Kime: Korean IME
+
+## IBus {#module-services-input-methods-ibus}
+
+IBus is an Intelligent Input Bus. It provides full featured and user
+friendly input method user interface.
+
+The following snippet can be used to configure IBus:
+
+```
+i18n.inputMethod = {
+  enabled = "ibus";
+  ibus.engines = with pkgs.ibus-engines; [ anthy hangul mozc ];
+};
+```
+
+`i18n.inputMethod.ibus.engines` is optional and can be used
+to add extra IBus engines.
+
+Available extra IBus engines are:
+
+  - Anthy (`ibus-engines.anthy`): Anthy is a system for
+    Japanese input method. It converts Hiragana text to Kana Kanji mixed text.
+  - Hangul (`ibus-engines.hangul`): Korean input method.
+  - m17n (`ibus-engines.m17n`): m17n is an input method that
+    uses input methods and corresponding icons in the m17n database.
+  - mozc (`ibus-engines.mozc`): A Japanese input method from
+    Google.
+  - Table (`ibus-engines.table`): An input method that load
+    tables of input methods.
+  - table-others (`ibus-engines.table-others`): Various
+    table-based input methods. To use this, and any other table-based input
+    methods, it must appear in the list of engines along with
+    `table`. For example:
+
+    ```
+    ibus.engines = with pkgs.ibus-engines; [ table table-others ];
+    ```
+
+To use any input method, the package must be added in the configuration, as
+shown above, and also (after running `nixos-rebuild`) the
+input method must be added from IBus' preference dialog.
+
+### Troubleshooting {#module-services-input-methods-troubleshooting}
+
+If IBus works in some applications but not others, a likely cause of this
+is that IBus is depending on a different version of `glib`
+to what the applications are depending on. This can be checked by running
+`nix-store -q --requisites <path> | grep glib`,
+where `<path>` is the path of either IBus or an
+application in the Nix store. The `glib` packages must
+match exactly. If they do not, uninstalling and reinstalling the
+application is a likely fix.
+
+## Fcitx5 {#module-services-input-methods-fcitx}
+
+Fcitx5 is an input method framework with extension support. It has three
+built-in Input Method Engine, Pinyin, QuWei and Table-based input methods.
+
+The following snippet can be used to configure Fcitx:
+
+```
+i18n.inputMethod = {
+  enabled = "fcitx5";
+  fcitx5.addons = with pkgs; [ fcitx5-mozc fcitx5-hangul fcitx5-m17n ];
+};
+```
+
+`i18n.inputMethod.fcitx5.addons` is optional and can be
+used to add extra Fcitx5 addons.
+
+Available extra Fcitx5 addons are:
+
+  - Anthy (`fcitx5-anthy`): Anthy is a system for
+    Japanese input method. It converts Hiragana text to Kana Kanji mixed text.
+  - Chewing (`fcitx5-chewing`): Chewing is an
+    intelligent Zhuyin input method. It is one of the most popular input
+    methods among Traditional Chinese Unix users.
+  - Hangul (`fcitx5-hangul`): Korean input method.
+  - Unikey (`fcitx5-unikey`): Vietnamese input method.
+  - m17n (`fcitx5-m17n`): m17n is an input method that
+    uses input methods and corresponding icons in the m17n database.
+  - mozc (`fcitx5-mozc`): A Japanese input method from
+    Google.
+  - table-others (`fcitx5-table-other`): Various
+    table-based input methods.
+  - chinese-addons (`fcitx5-chinese-addons`): Various chinese input methods.
+  - rime (`fcitx5-rime`): RIME support for fcitx5.
+
+## Nabi {#module-services-input-methods-nabi}
+
+Nabi is an easy to use Korean X input method. It allows you to enter
+phonetic Korean characters (hangul) and pictographic Korean characters
+(hanja).
+
+The following snippet can be used to configure Nabi:
+
+```
+i18n.inputMethod = {
+  enabled = "nabi";
+};
+```
+
+## Uim {#module-services-input-methods-uim}
+
+Uim (short for "universal input method") is a multilingual input method
+framework. Applications can use it through so-called bridges.
+
+The following snippet can be used to configure uim:
+
+```
+i18n.inputMethod = {
+  enabled = "uim";
+};
+```
+
+Note: The [](#opt-i18n.inputMethod.uim.toolbar) option can be
+used to choose uim toolbar.
+
+## Hime {#module-services-input-methods-hime}
+
+Hime is an extremely easy-to-use input method framework. It is lightweight,
+stable, powerful and supports many commonly used input methods, including
+Cangjie, Zhuyin, Dayi, Rank, Shrimp, Greek, Korean Pinyin, Latin Alphabet,
+etc...
+
+The following snippet can be used to configure Hime:
+
+```
+i18n.inputMethod = {
+  enabled = "hime";
+};
+```
+
+## Kime {#module-services-input-methods-kime}
+
+Kime is Korean IME. it's built with Rust language and let you get simple, safe, fast Korean typing
+
+The following snippet can be used to configure Kime:
+
+```
+i18n.inputMethod = {
+  enabled = "kime";
+};
+```
diff --git a/nixos/modules/i18n/input-method/default.nix b/nixos/modules/i18n/input-method/default.nix
index 07fb86bcc25..d967d4335c7 100644
--- a/nixos/modules/i18n/input-method/default.nix
+++ b/nixos/modules/i18n/input-method/default.nix
@@ -29,9 +29,9 @@ in
   options.i18n = {
     inputMethod = {
       enabled = mkOption {
-        type    = types.nullOr (types.enum [ "ibus" "fcitx" "fcitx5" "nabi" "uim" "hime" "kime" ]);
+        type    = types.nullOr (types.enum [ "ibus" "fcitx5" "nabi" "uim" "hime" "kime" ]);
         default = null;
-        example = "fcitx";
+        example = "fcitx5";
         description = lib.mdDoc ''
           Select the enabled input method. Input methods is a software to input symbols that are not available on standard input devices.
 
@@ -40,7 +40,6 @@ in
           Currently the following input methods are available in NixOS:
 
           - ibus: The intelligent input bus, extra input engines can be added using `i18n.inputMethod.ibus.engines`.
-          - fcitx: A customizable lightweight input method, extra input engines can be added using `i18n.inputMethod.fcitx.engines`.
           - fcitx5: The next generation of fcitx, addons (including engines, dictionaries, skins) can be added using `i18n.inputMethod.fcitx5.addons`.
           - nabi: A Korean input method based on XIM. Nabi doesn't support Qt 5.
           - uim: The universal input method, is a library with a XIM bridge. uim mainly support Chinese, Japanese and Korean.
@@ -66,7 +65,7 @@ in
 
   meta = {
     maintainers = with lib.maintainers; [ ericsagnes ];
-    doc = ./default.xml;
+    doc = ./default.md;
   };
 
 }
diff --git a/nixos/modules/i18n/input-method/default.xml b/nixos/modules/i18n/input-method/default.xml
deleted file mode 100644
index dd66316c730..00000000000
--- a/nixos/modules/i18n/input-method/default.xml
+++ /dev/null
@@ -1,291 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-input-methods">
- <title>Input Methods</title>
- <para>
-  Input methods are an operating system component that allows any data, such as
-  keyboard strokes or mouse movements, to be received as input. In this way
-  users can enter characters and symbols not found on their input devices.
-  Using an input method is obligatory for any language that has more graphemes
-  than there are keys on the keyboard.
- </para>
- <para>
-  The following input methods are available in NixOS:
- </para>
- <itemizedlist>
-  <listitem>
-   <para>
-    IBus: The intelligent input bus.
-   </para>
-  </listitem>
-  <listitem>
-   <para>
-    Fcitx: A customizable lightweight input method.
-   </para>
-  </listitem>
-  <listitem>
-   <para>
-    Nabi: A Korean input method based on XIM.
-   </para>
-  </listitem>
-  <listitem>
-   <para>
-    Uim: The universal input method, is a library with a XIM bridge.
-   </para>
-  </listitem>
-  <listitem>
-   <para>
-    Hime: An extremely easy-to-use input method framework.
-   </para>
-  </listitem>
-  <listitem>
-    <para>
-     Kime: Korean IME
-    </para>
-  </listitem>
- </itemizedlist>
- <section xml:id="module-services-input-methods-ibus">
-  <title>IBus</title>
-
-  <para>
-   IBus is an Intelligent Input Bus. It provides full featured and user
-   friendly input method user interface.
-  </para>
-
-  <para>
-   The following snippet can be used to configure IBus:
-  </para>
-
-<programlisting>
-i18n.inputMethod = {
-  <link linkend="opt-i18n.inputMethod.enabled">enabled</link> = "ibus";
-  <link linkend="opt-i18n.inputMethod.ibus.engines">ibus.engines</link> = with pkgs.ibus-engines; [ anthy hangul mozc ];
-};
-</programlisting>
-
-  <para>
-   <literal>i18n.inputMethod.ibus.engines</literal> is optional and can be used
-   to add extra IBus engines.
-  </para>
-
-  <para>
-   Available extra IBus engines are:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Anthy (<literal>ibus-engines.anthy</literal>): Anthy is a system for
-     Japanese input method. It converts Hiragana text to Kana Kanji mixed text.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Hangul (<literal>ibus-engines.hangul</literal>): Korean input method.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     m17n (<literal>ibus-engines.m17n</literal>): m17n is an input method that
-     uses input methods and corresponding icons in the m17n database.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     mozc (<literal>ibus-engines.mozc</literal>): A Japanese input method from
-     Google.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Table (<literal>ibus-engines.table</literal>): An input method that load
-     tables of input methods.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     table-others (<literal>ibus-engines.table-others</literal>): Various
-     table-based input methods. To use this, and any other table-based input
-     methods, it must appear in the list of engines along with
-     <literal>table</literal>. For example:
-<programlisting>
-ibus.engines = with pkgs.ibus-engines; [ table table-others ];
-</programlisting>
-    </para>
-   </listitem>
-  </itemizedlist>
-
-  <para>
-   To use any input method, the package must be added in the configuration, as
-   shown above, and also (after running <literal>nixos-rebuild</literal>) the
-   input method must be added from IBus' preference dialog.
-  </para>
-
-  <simplesect xml:id="module-services-input-methods-troubleshooting">
-   <title>Troubleshooting</title>
-   <para>
-    If IBus works in some applications but not others, a likely cause of this
-    is that IBus is depending on a different version of <literal>glib</literal>
-    to what the applications are depending on. This can be checked by running
-    <literal>nix-store -q --requisites &lt;path&gt; | grep glib</literal>,
-    where <literal>&lt;path&gt;</literal> is the path of either IBus or an
-    application in the Nix store. The <literal>glib</literal> packages must
-    match exactly. If they do not, uninstalling and reinstalling the
-    application is a likely fix.
-   </para>
-  </simplesect>
- </section>
- <section xml:id="module-services-input-methods-fcitx">
-  <title>Fcitx</title>
-
-  <para>
-   Fcitx is an input method framework with extension support. It has three
-   built-in Input Method Engine, Pinyin, QuWei and Table-based input methods.
-  </para>
-
-  <para>
-   The following snippet can be used to configure Fcitx:
-  </para>
-
-<programlisting>
-i18n.inputMethod = {
-  <link linkend="opt-i18n.inputMethod.enabled">enabled</link> = "fcitx";
-  <link linkend="opt-i18n.inputMethod.fcitx.engines">fcitx.engines</link> = with pkgs.fcitx-engines; [ mozc hangul m17n ];
-};
-</programlisting>
-
-  <para>
-   <literal>i18n.inputMethod.fcitx.engines</literal> is optional and can be
-   used to add extra Fcitx engines.
-  </para>
-
-  <para>
-   Available extra Fcitx engines are:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Anthy (<literal>fcitx-engines.anthy</literal>): Anthy is a system for
-     Japanese input method. It converts Hiragana text to Kana Kanji mixed text.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Chewing (<literal>fcitx-engines.chewing</literal>): Chewing is an
-     intelligent Zhuyin input method. It is one of the most popular input
-     methods among Traditional Chinese Unix users.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Hangul (<literal>fcitx-engines.hangul</literal>): Korean input method.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Unikey (<literal>fcitx-engines.unikey</literal>): Vietnamese input method.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     m17n (<literal>fcitx-engines.m17n</literal>): m17n is an input method that
-     uses input methods and corresponding icons in the m17n database.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     mozc (<literal>fcitx-engines.mozc</literal>): A Japanese input method from
-     Google.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     table-others (<literal>fcitx-engines.table-others</literal>): Various
-     table-based input methods.
-    </para>
-   </listitem>
-  </itemizedlist>
- </section>
- <section xml:id="module-services-input-methods-nabi">
-  <title>Nabi</title>
-
-  <para>
-   Nabi is an easy to use Korean X input method. It allows you to enter
-   phonetic Korean characters (hangul) and pictographic Korean characters
-   (hanja).
-  </para>
-
-  <para>
-   The following snippet can be used to configure Nabi:
-  </para>
-
-<programlisting>
-i18n.inputMethod = {
-  <link linkend="opt-i18n.inputMethod.enabled">enabled</link> = "nabi";
-};
-</programlisting>
- </section>
- <section xml:id="module-services-input-methods-uim">
-  <title>Uim</title>
-
-  <para>
-   Uim (short for "universal input method") is a multilingual input method
-   framework. Applications can use it through so-called bridges.
-  </para>
-
-  <para>
-   The following snippet can be used to configure uim:
-  </para>
-
-<programlisting>
-i18n.inputMethod = {
-  <link linkend="opt-i18n.inputMethod.enabled">enabled</link> = "uim";
-};
-</programlisting>
-
-  <para>
-   Note: The <xref linkend="opt-i18n.inputMethod.uim.toolbar"/> option can be
-   used to choose uim toolbar.
-  </para>
- </section>
- <section xml:id="module-services-input-methods-hime">
-  <title>Hime</title>
-
-  <para>
-   Hime is an extremely easy-to-use input method framework. It is lightweight,
-   stable, powerful and supports many commonly used input methods, including
-   Cangjie, Zhuyin, Dayi, Rank, Shrimp, Greek, Korean Pinyin, Latin Alphabet,
-   etc...
-  </para>
-
-  <para>
-   The following snippet can be used to configure Hime:
-  </para>
-
-<programlisting>
-i18n.inputMethod = {
-  <link linkend="opt-i18n.inputMethod.enabled">enabled</link> = "hime";
-};
-</programlisting>
- </section>
- <section xml:id="module-services-input-methods-kime">
-  <title>Kime</title>
-
-  <para>
-   Kime is Korean IME. it's built with Rust language and let you get simple, safe, fast Korean typing
-  </para>
-
-  <para>
-   The following snippet can be used to configure Kime:
-  </para>
-
-<programlisting>
-i18n.inputMethod = {
-  <link linkend="opt-i18n.inputMethod.enabled">enabled</link> = "kime";
-};
-</programlisting>
- </section>
-</chapter>
diff --git a/nixos/modules/i18n/input-method/fcitx.nix b/nixos/modules/i18n/input-method/fcitx.nix
deleted file mode 100644
index 043ec3d55c1..00000000000
--- a/nixos/modules/i18n/input-method/fcitx.nix
+++ /dev/null
@@ -1,46 +0,0 @@
-{ config, pkgs, lib, ... }:
-
-with lib;
-
-let
-  cfg = config.i18n.inputMethod.fcitx;
-  fcitxPackage = pkgs.fcitx.override { plugins = cfg.engines; };
-  fcitxEngine = types.package // {
-    name  = "fcitx-engine";
-    check = x: (lib.types.package.check x) && (attrByPath ["meta" "isFcitxEngine"] false x);
-  };
-in
-{
-  options = {
-
-    i18n.inputMethod.fcitx = {
-      engines = mkOption {
-        type    = with types; listOf fcitxEngine;
-        default = [];
-        example = literalExpression "with pkgs.fcitx-engines; [ mozc hangul ]";
-        description =
-          let
-            enginesDrv = filterAttrs (const isDerivation) pkgs.fcitx-engines;
-            engines = concatStringsSep ", "
-              (map (name: "`${name}`") (attrNames enginesDrv));
-          in
-            lib.mdDoc "Enabled Fcitx engines. Available engines are: ${engines}.";
-      };
-    };
-
-  };
-
-  config = mkIf (config.i18n.inputMethod.enabled == "fcitx") {
-    i18n.inputMethod.package = fcitxPackage;
-
-    environment.variables = {
-      GTK_IM_MODULE = "fcitx";
-      QT_IM_MODULE  = "fcitx";
-      XMODIFIERS    = "@im=fcitx";
-    };
-    services.xserver.displayManager.sessionCommands = "${fcitxPackage}/bin/fcitx";
-  };
-
-  # uses attributes of the linked package
-  meta.buildDocsInSandbox = false;
-}
diff --git a/nixos/modules/i18n/input-method/fcitx5.nix b/nixos/modules/i18n/input-method/fcitx5.nix
index aa816c90a3d..7251240d26a 100644
--- a/nixos/modules/i18n/input-method/fcitx5.nix
+++ b/nixos/modules/i18n/input-method/fcitx5.nix
@@ -5,10 +5,9 @@ with lib;
 let
   im = config.i18n.inputMethod;
   cfg = im.fcitx5;
-  addons = cfg.addons ++ optional cfg.enableRimeData pkgs.rime-data;
-  fcitx5Package = pkgs.fcitx5-with-addons.override { inherit addons; };
-  whetherRimeDataDir = any (p: p.pname == "fcitx5-rime") cfg.addons;
-in {
+  fcitx5Package = pkgs.fcitx5-with-addons.override { inherit (cfg) addons; };
+in
+{
   options = {
     i18n.inputMethod.fcitx5 = {
       addons = mkOption {
@@ -19,30 +18,23 @@ in {
           Enabled Fcitx5 addons.
         '';
       };
-
-      enableRimeData = mkEnableOption (lib.mdDoc "default rime-data with fcitx5-rime");
     };
   };
 
+  imports = [
+    (mkRemovedOptionModule [ "i18n" "inputMethod" "fcitx5" "enableRimeData" ] ''
+      RIME data is now included in `fcitx5-rime` by default, and can be customized using `fcitx5-rime.override { rimeDataPkgs = ...; }`
+    '')
+  ];
+
   config = mkIf (im.enabled == "fcitx5") {
     i18n.inputMethod.package = fcitx5Package;
 
-    environment = mkMerge [{
-      variables = {
-        GTK_IM_MODULE = "fcitx";
-        QT_IM_MODULE = "fcitx";
-        XMODIFIERS = "@im=fcitx";
-        QT_PLUGIN_PATH = [ "${fcitx5Package}/${pkgs.qt6.qtbase.qtPluginPrefix}" ];
-      };
-    }
-    (mkIf whetherRimeDataDir {
-      pathsToLink = [
-        "/share/rime-data"
-      ];
-
-      variables =  {
-        NIX_RIME_DATA_DIR = "/run/current-system/sw/share/rime-data";
-      };
-    })];
+    environment.variables = {
+      GTK_IM_MODULE = "fcitx";
+      QT_IM_MODULE = "fcitx";
+      XMODIFIERS = "@im=fcitx";
+      QT_PLUGIN_PATH = [ "${fcitx5Package}/${pkgs.qt6.qtbase.qtPluginPrefix}" ];
+    };
   };
 }
diff --git a/nixos/modules/i18n/input-method/ibus.nix b/nixos/modules/i18n/input-method/ibus.nix
index 520db128acd..2a35afad2ac 100644
--- a/nixos/modules/i18n/input-method/ibus.nix
+++ b/nixos/modules/i18n/input-method/ibus.nix
@@ -10,10 +10,7 @@ let
     check = x: (lib.types.package.check x) && (attrByPath ["meta" "isIbusEngine"] false x);
   };
 
-  impanel =
-    if cfg.panel != null
-    then "--panel=${cfg.panel}"
-    else "";
+  impanel = optionalString (cfg.panel != null) "--panel=${cfg.panel}";
 
   ibusAutostart = pkgs.writeTextFile {
     name = "autostart-ibus-daemon";
diff --git a/nixos/modules/i18n/input-method/kime.nix b/nixos/modules/i18n/input-method/kime.nix
index 29224a6bf75..e82996926b2 100644
--- a/nixos/modules/i18n/input-method/kime.nix
+++ b/nixos/modules/i18n/input-method/kime.nix
@@ -1,40 +1,37 @@
 { config, pkgs, lib, generators, ... }:
-with lib;
-let
-  cfg = config.i18n.inputMethod.kime;
-  yamlFormat = pkgs.formats.yaml { };
-in
-{
-  options = {
-    i18n.inputMethod.kime = {
-      config = mkOption {
-        type = yamlFormat.type;
-        default = { };
-        example = literalExpression ''
-          {
-            daemon = {
-              modules = ["Xim" "Indicator"];
-            };
+let imcfg = config.i18n.inputMethod;
+in {
+  imports = [
+    (lib.mkRemovedOptionModule [ "i18n" "inputMethod" "kime" "config" ] "Use i18n.inputMethod.kime.* instead")
+  ];
 
-            indicator = {
-              icon_color = "White";
-            };
-
-            engine = {
-              hangul = {
-                layout = "dubeolsik";
-              };
-            };
-          }
-          '';
-        description = lib.mdDoc ''
-          kime configuration. Refer to <https://github.com/Riey/kime/blob/v${pkgs.kime.version}/docs/CONFIGURATION.md> for details on supported values.
-        '';
-      };
+  options.i18n.inputMethod.kime = {
+    daemonModules = lib.mkOption {
+      type = lib.types.listOf (lib.types.enum [ "Xim" "Wayland" "Indicator" ]);
+      default = [ "Xim" "Wayland" "Indicator" ];
+      example = [ "Xim" "Indicator" ];
+      description = lib.mdDoc ''
+        List of enabled daemon modules
+      '';
+    };
+    iconColor = lib.mkOption {
+      type = lib.types.enum [ "Black" "White" ];
+      default = "Black";
+      example = "White";
+      description = lib.mdDoc ''
+        Color of the indicator icon
+      '';
+    };
+    extraConfig = lib.mkOption {
+      type = lib.types.lines;
+      default = "";
+      description = lib.mdDoc ''
+        extra kime configuration. Refer to <https://github.com/Riey/kime/blob/v${pkgs.kime.version}/docs/CONFIGURATION.md> for details on supported values.
+      '';
     };
   };
 
-  config = mkIf (config.i18n.inputMethod.enabled == "kime") {
+  config = lib.mkIf (imcfg.enabled == "kime") {
     i18n.inputMethod.package = pkgs.kime;
 
     environment.variables = {
@@ -43,7 +40,12 @@ in
       XMODIFIERS    = "@im=kime";
     };
 
-    environment.etc."xdg/kime/config.yaml".text = replaceStrings [ "\\\\" ] [ "\\" ] (builtins.toJSON cfg.config);
+    environment.etc."xdg/kime/config.yaml".text = ''
+      daemon:
+        modules: [${lib.concatStringsSep "," imcfg.kime.daemonModules}]
+      indicator:
+        icon_color: ${imcfg.kime.iconColor}
+    '' + imcfg.kime.extraConfig;
   };
 
   # uses attributes of the linked package
diff --git a/nixos/modules/installer/cd-dvd/channel.nix b/nixos/modules/installer/cd-dvd/channel.nix
index 4b4c2e39334..8426ba8fac0 100644
--- a/nixos/modules/installer/cd-dvd/channel.nix
+++ b/nixos/modules/installer/cd-dvd/channel.nix
@@ -42,7 +42,7 @@ in
   # see discussion in https://github.com/NixOS/nixpkgs/pull/204178#issuecomment-1336289021
   nix.registry.nixpkgs.to = {
     type = "path";
-    path = nixpkgs;
+    path = "${channelSources}/nixos";
   };
 
   # Provide the NixOS/Nixpkgs sources in /etc/nixos.  This is required
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix
index d015e10c11d..12feb2d96ec 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix
@@ -31,7 +31,7 @@
   };
 
   # Theme calamares with GNOME theme
-  qt5 = {
+  qt = {
     enable = true;
     platformTheme = "gnome";
   };
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
index 573b31b439c..ea8056ff870 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
@@ -6,6 +6,7 @@
   imports = [ ./installation-cd-graphical-base.nix ];
 
   isoImage.edition = "gnome";
+  isoImage.graphicalGrub = true;
 
   services.xserver.desktopManager.gnome = {
     # Add Firefox and other tools useful for installation to the launcher
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix b/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix
index 7a3bd74cb70..29afdd47109 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix
@@ -14,6 +14,10 @@
 
   documentation.man.enable = lib.mkOverride 500 true;
 
+  # Although we don't really need HTML documentation in the minimal installer,
+  # not including it may cause annoying cache misses in the case of the NixOS manual.
+  documentation.doc.enable = lib.mkOverride 500 true;
+
   fonts.fontconfig.enable = lib.mkForce false;
 
   isoImage.edition = lib.mkForce "minimal";
diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index 659df7851b0..c430048d659 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -22,8 +22,8 @@ let
       (option: ''
         menuentry '${defaults.name} ${
         # Name appended to menuentry defaults to params if no specific name given.
-        option.name or (if option ? params then "(${option.params})" else "")
-        }' ${if option ? class then " --class ${option.class}" else ""} {
+        option.name or (optionalString (option ? params) "(${option.params})")
+        }' ${optionalString (option ? class) " --class ${option.class}"} {
           linux ${defaults.image} \''${isoboot} ${defaults.params} ${
             option.params or ""
           }
@@ -52,7 +52,7 @@ let
   buildMenuAdditionalParamsGrub2 = additional:
   let
     finalCfg = {
-      name = "NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel}";
+      name = "${config.isoImage.prependToMenuLabel}${config.system.nixos.distroName} ${config.system.nixos.label}${config.isoImage.appendToMenuLabel}";
       params = "init=${config.system.build.toplevel}/init ${additional} ${toString config.boot.kernelParams}";
       image = "/boot/${config.system.boot.loader.kernelFile}";
       initrd = "/boot/initrd";
@@ -109,35 +109,35 @@ let
     DEFAULT boot
 
     LABEL boot
-    MENU LABEL NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel}
+    MENU LABEL ${config.isoImage.prependToMenuLabel}${config.system.nixos.distroName} ${config.system.nixos.label}${config.isoImage.appendToMenuLabel}
     LINUX /boot/${config.system.boot.loader.kernelFile}
     APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}
     INITRD /boot/${config.system.boot.loader.initrdFile}
 
     # A variant to boot with 'nomodeset'
     LABEL boot-nomodeset
-    MENU LABEL NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (nomodeset)
+    MENU LABEL ${config.isoImage.prependToMenuLabel}${config.system.nixos.distroName} ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (nomodeset)
     LINUX /boot/${config.system.boot.loader.kernelFile}
     APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} nomodeset
     INITRD /boot/${config.system.boot.loader.initrdFile}
 
     # A variant to boot with 'copytoram'
     LABEL boot-copytoram
-    MENU LABEL NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (copytoram)
+    MENU LABEL ${config.isoImage.prependToMenuLabel}${config.system.nixos.distroName} ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (copytoram)
     LINUX /boot/${config.system.boot.loader.kernelFile}
     APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} copytoram
     INITRD /boot/${config.system.boot.loader.initrdFile}
 
     # A variant to boot with verbose logging to the console
     LABEL boot-debug
-    MENU LABEL NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (debug)
+    MENU LABEL ${config.isoImage.prependToMenuLabel}${config.system.nixos.distroName} ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (debug)
     LINUX /boot/${config.system.boot.loader.kernelFile}
     APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} loglevel=7
     INITRD /boot/${config.system.boot.loader.initrdFile}
 
     # A variant to boot with a serial console enabled
     LABEL boot-serial
-    MENU LABEL NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (serial console=ttyS0,115200n8)
+    MENU LABEL ${config.isoImage.prependToMenuLabel}${config.system.nixos.distroName} ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (serial console=ttyS0,115200n8)
     LINUX /boot/${config.system.boot.loader.kernelFile}
     APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} console=ttyS0,115200n8
     INITRD /boot/${config.system.boot.loader.initrdFile}
@@ -283,7 +283,7 @@ let
     cat <<EOF > $out/EFI/boot/grub.cfg
 
     set with_fonts=false
-    set textmode=false
+    set textmode=${boolToString (!config.isoImage.graphicalGrub)}
     # If you want to use serial for "terminal_*" commands, you need to set one up:
     #   Example manual configuration:
     #    → serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
@@ -442,9 +442,6 @@ let
       fsck.vfat -vn "$out"
     ''; # */
 
-  # Syslinux (and isolinux) only supports x86-based architectures.
-  canx86BiosBoot = pkgs.stdenv.hostPlatform.isx86;
-
 in
 
 {
@@ -452,13 +449,15 @@ in
 
     isoImage.isoName = mkOption {
       default = "${config.isoImage.isoBaseName}.iso";
+      type = lib.types.str;
       description = lib.mdDoc ''
         Name of the generated ISO image file.
       '';
     };
 
     isoImage.isoBaseName = mkOption {
-      default = "nixos";
+      default = config.system.nixos.distroId;
+      type = lib.types.str;
       description = lib.mdDoc ''
         Prefix of the name of the generated ISO image file.
       '';
@@ -466,6 +465,7 @@ in
 
     isoImage.compressImage = mkOption {
       default = false;
+      type = lib.types.bool;
       description = lib.mdDoc ''
         Whether the ISO image should be compressed using
         {command}`zstd`.
@@ -473,12 +473,13 @@ in
     };
 
     isoImage.squashfsCompression = mkOption {
-      default = with pkgs.stdenv.targetPlatform; "xz -Xdict-size 100% "
+      default = with pkgs.stdenv.hostPlatform; "xz -Xdict-size 100% "
                 + lib.optionalString isx86 "-Xbcj x86"
                 # Untested but should also reduce size for these platforms
                 + lib.optionalString isAarch "-Xbcj arm"
                 + lib.optionalString (isPower && is32bit && isBigEndian) "-Xbcj powerpc"
                 + lib.optionalString (isSparc) "-Xbcj sparc";
+      type = lib.types.str;
       description = lib.mdDoc ''
         Compression settings to use for the squashfs nix store.
       '';
@@ -487,6 +488,7 @@ in
 
     isoImage.edition = mkOption {
       default = "";
+      type = lib.types.str;
       description = lib.mdDoc ''
         Specifies which edition string to use in the volume ID of the generated
         ISO image.
@@ -496,6 +498,7 @@ in
     isoImage.volumeID = mkOption {
       # nixos-$EDITION-$RELEASE-$ARCH
       default = "nixos${optionalString (config.isoImage.edition != "") "-${config.isoImage.edition}"}-${config.system.nixos.release}-${pkgs.stdenv.hostPlatform.uname.processor}";
+      type = lib.types.str;
       description = lib.mdDoc ''
         Specifies the label or volume ID of the generated ISO image.
         Note that the label is used by stage 1 of the boot process to
@@ -526,6 +529,7 @@ in
 
     isoImage.includeSystemBuildDependencies = mkOption {
       default = false;
+      type = lib.types.bool;
       description = lib.mdDoc ''
         Set this option to include all the needed sources etc in the
         image. It significantly increases image size. Use that when
@@ -535,15 +539,35 @@ in
       '';
     };
 
+    isoImage.makeBiosBootable = mkOption {
+      # Before this option was introduced, images were BIOS-bootable if the
+      # hostPlatform was x86-based. This option is enabled by default for
+      # backwards compatibility.
+      #
+      # Also note that syslinux package currently cannot be cross-compiled from
+      # non-x86 platforms, so the default is false on non-x86 build platforms.
+      default = pkgs.stdenv.buildPlatform.isx86 && pkgs.stdenv.hostPlatform.isx86;
+      defaultText = lib.literalMD ''
+        `true` if both build and host platforms are x86-based architectures,
+        e.g. i686 and x86_64.
+      '';
+      type = lib.types.bool;
+      description = lib.mdDoc ''
+        Whether the ISO image should be a BIOS-bootable disk.
+      '';
+    };
+
     isoImage.makeEfiBootable = mkOption {
       default = false;
+      type = lib.types.bool;
       description = lib.mdDoc ''
-        Whether the ISO image should be an efi-bootable volume.
+        Whether the ISO image should be an EFI-bootable volume.
       '';
     };
 
     isoImage.makeUsbBootable = mkOption {
       default = false;
+      type = lib.types.bool;
       description = lib.mdDoc ''
         Whether the ISO image should be bootable from CD as well as USB.
       '';
@@ -579,7 +603,7 @@ in
 
     isoImage.syslinuxTheme = mkOption {
       default = ''
-        MENU TITLE NixOS
+        MENU TITLE ${config.system.nixos.distroName}
         MENU RESOLUTION 800 600
         MENU CLEAR
         MENU ROWS 6
@@ -608,8 +632,22 @@ in
       '';
     };
 
+    isoImage.prependToMenuLabel = mkOption {
+      default = "";
+      type = types.str;
+      example = "Install ";
+      description = lib.mdDoc ''
+        The string to prepend before the menu label for the NixOS system.
+        This will be directly prepended (without whitespace) to the NixOS version
+        string, like for example if it is set to `XXX`:
+
+        `XXXNixOS 99.99-pre666`
+      '';
+    };
+
     isoImage.appendToMenuLabel = mkOption {
       default = " Installer";
+      type = types.str;
       example = " Live System";
       description = lib.mdDoc ''
         The string to append after the menu label for the NixOS system.
@@ -620,6 +658,16 @@ in
       '';
     };
 
+    isoImage.graphicalGrub = mkOption {
+      default = false;
+      type = types.bool;
+      example = true;
+      description = lib.mdDoc ''
+        Whether to use textmode or graphical grub.
+        false means we use textmode grub.
+      '';
+    };
+
   };
 
   # store them in lib so we can mkImageMediaOverride the
@@ -674,6 +722,11 @@ in
   config = {
     assertions = [
       {
+        # Syslinux (and isolinux) only supports x86-based architectures.
+        assertion = config.isoImage.makeBiosBootable -> pkgs.stdenv.hostPlatform.isx86;
+        message = "BIOS boot is only supported on x86-based architectures.";
+      }
+      {
         assertion = !(stringLength config.isoImage.volumeID > 32);
         # https://wiki.osdev.org/ISO_9660#The_Primary_Volume_Descriptor
         # Volume Identifier can only be 32 bytes
@@ -686,14 +739,12 @@ in
       }
     ];
 
-    boot.loader.grub.version = 2;
-
     # Don't build the GRUB menu builder script, since we don't need it
     # here and it causes a cyclic dependency.
     boot.loader.grub.enable = false;
 
     environment.systemPackages =  [ grubPkgs.grub2 grubPkgs.grub2_efi ]
-      ++ optional canx86BiosBoot pkgs.syslinux
+      ++ optional (config.isoImage.makeBiosBootable) pkgs.syslinux
     ;
 
     # In stage 1 of the boot, mount the CD as the root FS by label so
@@ -744,7 +795,7 @@ in
         { source = pkgs.writeText "version" config.system.nixos.label;
           target = "/version.txt";
         }
-      ] ++ optionals canx86BiosBoot [
+      ] ++ optionals (config.isoImage.makeBiosBootable) [
         { source = config.isoImage.splashImage;
           target = "/isolinux/background.png";
         }
@@ -771,7 +822,7 @@ in
         { source = config.isoImage.efiSplashImage;
           target = "/EFI/boot/efi-background.png";
         }
-      ] ++ optionals (config.boot.loader.grub.memtest86.enable && canx86BiosBoot) [
+      ] ++ optionals (config.boot.loader.grub.memtest86.enable && config.isoImage.makeBiosBootable) [
         { source = "${pkgs.memtest86plus}/memtest.bin";
           target = "/boot/memtest.bin";
         }
@@ -786,10 +837,10 @@ in
     # Create the ISO image.
     system.build.isoImage = pkgs.callPackage ../../../lib/make-iso9660-image.nix ({
       inherit (config.isoImage) isoName compressImage volumeID contents;
-      bootable = canx86BiosBoot;
+      bootable = config.isoImage.makeBiosBootable;
       bootImage = "/isolinux/isolinux.bin";
-      syslinux = if canx86BiosBoot then pkgs.syslinux else null;
-    } // optionalAttrs (config.isoImage.makeUsbBootable && canx86BiosBoot) {
+      syslinux = if config.isoImage.makeBiosBootable then pkgs.syslinux else null;
+    } // optionalAttrs (config.isoImage.makeUsbBootable && config.isoImage.makeBiosBootable) {
       usbBootable = true;
       isohybridMbrImage = "${pkgs.syslinux}/share/syslinux/isohdpfx.bin";
     } // optionalAttrs config.isoImage.makeEfiBootable {
diff --git a/nixos/modules/installer/netboot/netboot-minimal.nix b/nixos/modules/installer/netboot/netboot-minimal.nix
index 91065d52faf..5ca255acf35 100644
--- a/nixos/modules/installer/netboot/netboot-minimal.nix
+++ b/nixos/modules/installer/netboot/netboot-minimal.nix
@@ -9,4 +9,7 @@
   ];
 
   documentation.man.enable = lib.mkOverride 500 true;
+  hardware.enableRedistributableFirmware = lib.mkOverride 70 false;
+  system.extraDependencies = lib.mkOverride 70 [];
+  networking.wireless.enable = lib.mkOverride 500 false;
 }
diff --git a/nixos/modules/installer/netboot/netboot.nix b/nixos/modules/installer/netboot/netboot.nix
index 03bb529cd85..a50f22cbe47 100644
--- a/nixos/modules/installer/netboot/netboot.nix
+++ b/nixos/modules/installer/netboot/netboot.nix
@@ -8,6 +8,20 @@ with lib;
 {
   options = {
 
+    netboot.squashfsCompression = mkOption {
+      default = with pkgs.stdenv.hostPlatform; "xz -Xdict-size 100% "
+                + lib.optionalString isx86 "-Xbcj x86"
+                # Untested but should also reduce size for these platforms
+                + lib.optionalString isAarch "-Xbcj arm"
+                + lib.optionalString (isPower && is32bit && isBigEndian) "-Xbcj powerpc"
+                + lib.optionalString (isSparc) "-Xbcj sparc";
+      description = lib.mdDoc ''
+        Compression settings to use for the squashfs nix store.
+      '';
+      example = "zstd -Xcompression-level 6";
+      type = types.str;
+    };
+
     netboot.storeContents = mkOption {
       example = literalExpression "[ pkgs.stdenv ]";
       description = lib.mdDoc ''
@@ -25,9 +39,7 @@ with lib;
 
     # !!! Hack - attributes expected by other modules.
     environment.systemPackages = [ pkgs.grub2_efi ]
-      ++ (if pkgs.stdenv.hostPlatform.system == "aarch64-linux"
-          then []
-          else [ pkgs.grub2 pkgs.syslinux ]);
+      ++ (lib.optionals (pkgs.stdenv.hostPlatform.system != "aarch64-linux") [pkgs.grub2 pkgs.syslinux]);
 
     fileSystems."/" = mkImageMediaOverride
       { fsType = "tmpfs";
@@ -77,6 +89,7 @@ with lib;
     # Create the squashfs image that contains the Nix store.
     system.build.squashfsStore = pkgs.callPackage ../../../lib/make-squashfs.nix {
       storeContents = config.netboot.storeContents;
+      comp = config.netboot.squashfsCompression;
     };
 
 
diff --git a/nixos/modules/installer/sd-card/sd-image-powerpc64le.nix b/nixos/modules/installer/sd-card/sd-image-powerpc64le.nix
new file mode 100644
index 00000000000..143c678e43f
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-powerpc64le.nix
@@ -0,0 +1,49 @@
+# To build, use:
+# nix-build nixos -I nixos-config=nixos/modules/installer/sd-card/sd-image-powerpc64le.nix -A config.system.build.sdImage
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ../../profiles/base.nix
+    ../../profiles/installation-device.nix
+    ./sd-image.nix
+  ];
+
+  boot.loader = {
+    # powerpc64le-linux typically uses petitboot
+    grub.enable = false;
+    generic-extlinux-compatible = {
+      # petitboot is not does not support all of the extlinux extensions to
+      # syslinux, but its parser is very forgiving; it essentially ignores
+      # whatever it doesn't understand.  See below for a filename adjustment.
+      enable = true;
+    };
+  };
+
+  boot.consoleLogLevel = lib.mkDefault 7;
+  boot.kernelParams = [ "console=hvc0" ];
+
+  sdImage = {
+    populateFirmwareCommands = "";
+    populateRootCommands = ''
+      mkdir -p ./files/boot
+      ${config.boot.loader.generic-extlinux-compatible.populateCmd} \
+        -c ${config.system.build.toplevel} \
+        -d ./files/boot
+    ''
+    # https://github.com/open-power/petitboot/blob/master/discover/syslinux-parser.c
+    # petitboot will look in these paths (plus all-caps versions of them):
+    #  /boot/syslinux/syslinux.cfg
+    #  /syslinux/syslinux.cfg
+    #  /syslinux.cfg
+    + ''
+      mv ./files/boot/extlinux ./files/boot/syslinux
+      mv ./files/boot/syslinux/extlinux.conf ./files/boot/syslinux/syslinux.cfg
+    ''
+    # petitboot does not support relative paths for LINUX or INITRD; it prepends
+    # a `/` when parsing these fields
+    + ''
+      sed -i 's_^\(\W\W*\(INITRD\|initrd\|LINUX\|linux\)\W\)\.\./_\1/boot/_' ./files/boot/syslinux/syslinux.cfg
+    '';
+  };
+}
diff --git a/nixos/modules/installer/tools/nix-fallback-paths.nix b/nixos/modules/installer/tools/nix-fallback-paths.nix
index 3eca901bdbf..582334a5aea 100644
--- a/nixos/modules/installer/tools/nix-fallback-paths.nix
+++ b/nixos/modules/installer/tools/nix-fallback-paths.nix
@@ -1,7 +1,7 @@
 {
-  x86_64-linux = "/nix/store/h88w1442c7hzkbw8sgpcsbqp4lhz6l5p-nix-2.12.0";
-  i686-linux = "/nix/store/j23527l1c3hfx17nssc0v53sq6c741zs-nix-2.12.0";
-  aarch64-linux = "/nix/store/zgzmdymyh934y3r4vqh8z337ba4cwsjb-nix-2.12.0";
-  x86_64-darwin = "/nix/store/wnlrzllazdyg1nrw9na497p4w0m7i7mm-nix-2.12.0";
-  aarch64-darwin = "/nix/store/7n5yamgzg5dpp5vb6ipdqgfh6cf30wmn-nix-2.12.0";
+  x86_64-linux = "/nix/store/ny9r65799s7xhp605bc2753sjvzkxrrs-nix-2.15.1";
+  i686-linux = "/nix/store/ck55dz5klc7szi8rx9ghhm8gi2b5q5bw-nix-2.15.1";
+  aarch64-linux = "/nix/store/cl0a02vr28913dgw98hrm45a4baqr3z1-nix-2.15.1";
+  x86_64-darwin = "/nix/store/wq228jdbz16pp2lnxf32n8dv27pw53p8-nix-2.15.1";
+  aarch64-darwin = "/nix/store/x11cpsjg4q236msfz5scc325pfp9xy64-nix-2.15.1";
 }
diff --git a/nixos/modules/installer/tools/nixos-enter.sh b/nixos/modules/installer/tools/nixos-enter.sh
index 30113ee0050..9141cc28570 100644..100755
--- a/nixos/modules/installer/tools/nixos-enter.sh
+++ b/nixos/modules/installer/tools/nixos-enter.sh
@@ -97,11 +97,12 @@ chroot_add_resolv_conf "$mountPoint" || echo "$0: failed to set up resolv.conf"
         exec 2>/dev/null
     fi
 
-    # Run the activation script. Set $LOCALE_ARCHIVE to supress some Perl locale warnings.
+    # Run the activation script. Set $LOCALE_ARCHIVE to suppress some Perl locale warnings.
     LOCALE_ARCHIVE="$system/sw/lib/locale/locale-archive" IN_NIXOS_ENTER=1 chroot "$mountPoint" "$system/activate" 1>&2 || true
 
-    # Create /tmp
-    chroot "$mountPoint" "$system/sw/bin/systemd-tmpfiles" --create --remove --exclude-prefix=/dev 1>&2 || true
+    # Create /tmp. This is needed for nix-build and the NixOS activation script to work.
+    # Hide the unhelpful "failed to replace specifiers" errors caused by missing /etc/machine-id.
+    chroot "$mountPoint" "$system/sw/bin/systemd-tmpfiles" --create --remove -E 2> /dev/null || true
 )
 
 unset TMPDIR
diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl
index 212b2b3cd23..2e572ef0247 100644
--- a/nixos/modules/installer/tools/nixos-generate-config.pl
+++ b/nixos/modules/installer/tools/nixos-generate-config.pl
@@ -85,12 +85,7 @@ sub debug {
 
 
 # nixpkgs.system
-my ($status, @systemLines) = runCommand("@nixInstantiate@ --impure --eval --expr builtins.currentSystem");
-if ($status != 0 || join("", @systemLines) =~ /error/) {
-    die "Failed to retrieve current system type from nix.\n";
-}
-chomp(my $system = @systemLines[0]);
-push @attrs, "nixpkgs.hostPlatform = lib.mkDefault $system;";
+push @attrs, "nixpkgs.hostPlatform = lib.mkDefault \"@hostPlatformSystem@\";";
 
 
 my $cpuinfo = read_file "/proc/cpuinfo";
@@ -127,9 +122,6 @@ if (-e "/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors") {
 push @kernelModules, "kvm-intel" if hasCPUFeature "vmx";
 push @kernelModules, "kvm-amd" if hasCPUFeature "svm";
 
-push @attrs, "hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;" if cpuManufacturer "AuthenticAMD";
-push @attrs, "hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;" if cpuManufacturer "GenuineIntel";
-
 
 # Look at the PCI devices and add necessary modules.  Note that most
 # modules are auto-detected so we don't need to list them here.
@@ -203,7 +195,7 @@ sub pciCheck {
     }
 
     # In case this is a virtio scsi device, we need to explicitly make this available.
-    if ($vendor eq "0x1af4" && $device eq "0x1004") {
+    if ($vendor eq "0x1af4" && ($device eq "0x1004" || $device eq "0x1048") ) {
         push @initrdAvailableKernelModules, "virtio_scsi";
     }
 
@@ -324,11 +316,15 @@ if ($virt eq "systemd-nspawn") {
 }
 
 
-# Provide firmware for devices that are not detected by this script,
-# unless we're in a VM/container.
-push @imports, "(modulesPath + \"/installer/scan/not-detected.nix\")"
-    if $virt eq "none";
+# Check if we're on bare metal, not in a VM/container.
+if ($virt eq "none") {
+    # Provide firmware for devices that are not detected by this script.
+    push @imports, "(modulesPath + \"/installer/scan/not-detected.nix\")";
 
+    # Update the microcode.
+    push @attrs, "hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;" if cpuManufacturer "AuthenticAMD";
+    push @attrs, "hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;" if cpuManufacturer "GenuineIntel";
+}
 
 # For a device name like /dev/sda1, find a more stable path like
 # /dev/disk/by-uuid/X or /dev/disk/by-label/Y.
@@ -339,7 +335,7 @@ sub findStableDevPath {
 
     my $st = stat($dev) or return $dev;
 
-    foreach my $dev2 (glob("/dev/disk/by-uuid/*"), glob("/dev/mapper/*"), glob("/dev/disk/by-label/*")) {
+    foreach my $dev2 (glob("/dev/stratis/*/*"), glob("/dev/disk/by-uuid/*"), glob("/dev/mapper/*"), glob("/dev/disk/by-label/*")) {
         my $st2 = stat($dev2) or next;
         return $dev2 if $st->rdev == $st2->rdev;
     }
@@ -471,14 +467,25 @@ EOF
         }
     }
 
+    # is this a stratis fs?
+    my $stableDevPath = findStableDevPath $device;
+    my $stratisPool;
+    if ($stableDevPath =~ qr#/dev/stratis/(.*)/.*#) {
+        my $poolName = $1;
+        my ($header, @lines) = split "\n", qx/stratis pool list/;
+        my $uuidIndex = index $header, 'UUID';
+        my ($line) = grep /^$poolName /, @lines;
+        $stratisPool = substr $line, $uuidIndex - 32, 36;
+    }
+
     # Don't emit tmpfs entry for /tmp, because it most likely comes from the
-    # boot.tmpOnTmpfs option in configuration.nix (managed declaratively).
+    # boot.tmp.useTmpfs option in configuration.nix (managed declaratively).
     next if ($mountPoint eq "/tmp" && $fsType eq "tmpfs");
 
     # Emit the filesystem.
     $fileSystems .= <<EOF;
   fileSystems.\"$mountPoint\" =
-    { device = \"${\(findStableDevPath $device)}\";
+    { device = \"$stableDevPath\";
       fsType = \"$fsType\";
 EOF
 
@@ -488,6 +495,12 @@ EOF
 EOF
     }
 
+    if ($stratisPool) {
+        $fileSystems .= <<EOF;
+      stratis.poolUuid = "$stratisPool";
+EOF
+    }
+
     $fileSystems .= <<EOF;
     };
 
@@ -517,21 +530,6 @@ EOF
     }
 }
 
-# For lack of a better way to determine it, guess whether we should use a
-# bigger font for the console from the display mode on the first
-# framebuffer. A way based on the physical size/actual DPI reported by
-# the monitor would be nice, but I don't know how to do this without X :)
-my $fb_modes_file = "/sys/class/graphics/fb0/modes";
-if (-f $fb_modes_file && -r $fb_modes_file) {
-    my $modes = read_file($fb_modes_file);
-    $modes =~ m/([0-9]+)x([0-9]+)/;
-    my $console_width = $1, my $console_height = $2;
-    if ($console_width > 1920) {
-        push @attrs, "# high-resolution display";
-        push @attrs, 'hardware.video.hidpi.enable = lib.mkDefault true;';
-    }
-}
-
 
 # Generate the hardware configuration file.
 
@@ -670,7 +668,6 @@ EOF
             $bootLoaderConfig = <<EOF;
   # Use the GRUB 2 boot loader.
   boot.loader.grub.enable = true;
-  boot.loader.grub.version = 2;
   # boot.loader.grub.efiSupport = true;
   # boot.loader.grub.efiInstallAsRemovable = true;
   # boot.loader.efi.efiSysMountPoint = "/boot/efi";
diff --git a/nixos/modules/installer/tools/nixos-install.sh b/nixos/modules/installer/tools/nixos-install.sh
index e7cf52f5e32..20fec525e70 100644..100755
--- a/nixos/modules/installer/tools/nixos-install.sh
+++ b/nixos/modules/installer/tools/nixos-install.sh
@@ -195,7 +195,20 @@ if [[ -z $noBootLoader ]]; then
     echo "installing the boot loader..."
     # Grub needs an mtab.
     ln -sfn /proc/mounts "$mountPoint"/etc/mtab
-    NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root "$mountPoint" -- /run/current-system/bin/switch-to-configuration boot
+    export mountPoint
+    NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root "$mountPoint" -c "$(cat <<'EOF'
+      # Create a bind mount for each of the mount points inside the target file
+      # system. This preserves the validity of their absolute paths after changing
+      # the root with `nixos-enter`.
+      # Without this the bootloader installation may fail due to options that
+      # contain paths referenced during evaluation, like initrd.secrets.
+      # when not root, re-execute the script in an unshared namespace
+      mount --rbind --mkdir / "$mountPoint"
+      mount --make-rslave "$mountPoint"
+      /run/current-system/bin/switch-to-configuration boot
+      umount -R "$mountPoint" && rmdir "$mountPoint"
+EOF
+)"
 fi
 
 # Ask the user to set a root password, but only if the passwd command
diff --git a/nixos/modules/installer/tools/nixos-version.sh b/nixos/modules/installer/tools/nixos-version.sh
index 59a9c572b41..39e34a3718c 100644
--- a/nixos/modules/installer/tools/nixos-version.sh
+++ b/nixos/modules/installer/tools/nixos-version.sh
@@ -8,11 +8,18 @@ case "$1" in
     ;;
   --hash|--revision)
     if ! [[ @revision@ =~ ^[0-9a-f]+$ ]]; then
-      echo "$0: Nixpkgs commit hash is unknown"
+      echo "$0: Nixpkgs commit hash is unknown" >&2
       exit 1
     fi
     echo "@revision@"
     ;;
+  --configuration-revision)
+    if [[ "@configurationRevision@" =~ "@" ]]; then
+      echo "$0: configuration revision is unknown" >&2
+      exit 1
+    fi
+    echo "@configurationRevision@"
+    ;;
   --json)
     cat <<EOF
 @json@
diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix
index e46a2df8fa6..54b0f81ee78 100644
--- a/nixos/modules/installer/tools/tools.nix
+++ b/nixos/modules/installer/tools/tools.nix
@@ -25,6 +25,7 @@ let
     path = makeBinPath [
       pkgs.jq
       nixos-enter
+      pkgs.util-linuxMinimal
     ];
   };
 
@@ -34,17 +35,14 @@ let
     name = "nixos-generate-config";
     src = ./nixos-generate-config.pl;
     perl = "${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl";
-    nixInstantiate = "${pkgs.nix}/bin/nix-instantiate";
+    hostPlatformSystem = pkgs.stdenv.hostPlatform.system;
     detectvirt = "${config.systemd.package}/bin/systemd-detect-virt";
     btrfs = "${pkgs.btrfs-progs}/bin/btrfs";
     inherit (config.system.nixos-generate-config) configuration desktopConfiguration;
     xserverEnabled = config.services.xserver.enable;
   };
 
-  nixos-option =
-    if lib.versionAtLeast (lib.getVersion config.nix.package) "2.4pre"
-    then null
-    else pkgs.nixos-option;
+  inherit (pkgs) nixos-option;
 
   nixos-version = makeProg {
     name = "nixos-version";
@@ -65,6 +63,9 @@ let
     name = "nixos-enter";
     src = ./nixos-enter.sh;
     inherit (pkgs) runtimeShell;
+    path = makeBinPath [
+      pkgs.util-linuxMinimal
+    ];
   };
 
 in
@@ -123,7 +124,7 @@ in
     system.nixos-generate-config.configuration = mkDefault ''
       # Edit this configuration file to define what should be installed on
       # your system.  Help is available in the configuration.nix(5) man page
-      # and in the NixOS manual (accessible by running ‘nixos-help’).
+      # and in the NixOS manual (accessible by running `nixos-help`).
 
       { config, pkgs, ... }:
 
@@ -159,10 +160,7 @@ in
       $desktopConfiguration
         # Configure keymap in X11
         # services.xserver.layout = "us";
-        # services.xserver.xkbOptions = {
-        #   "eurosign:e";
-        #   "caps:escape" # map caps to escape.
-        # };
+        # services.xserver.xkbOptions = "eurosign:e,caps:escape";
 
         # Enable CUPS to print documents.
         # services.printing.enable = true;
@@ -180,7 +178,7 @@ in
         #   extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
         #   packages = with pkgs; [
         #     firefox
-        #     thunderbird
+        #     tree
         #   ];
         # };
 
@@ -217,7 +215,7 @@ in
 
         # This value determines the NixOS release from which the default
         # settings for stateful data, like file locations and database versions
-        # on your system were taken. It‘s perfectly fine and recommended to leave
+        # on your system were taken. It's perfectly fine and recommended to leave
         # this value at the release version of the first install of this system.
         # Before changing this value read the documentation for this option
         # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
@@ -231,9 +229,12 @@ in
         nixos-install
         nixos-rebuild
         nixos-generate-config
+        nixos-option
         nixos-version
         nixos-enter
-      ] ++ lib.optional (nixos-option != null) nixos-option;
+      ];
+
+    documentation.man.man-db.skipPackages = [ nixos-version ];
 
     system.build = {
       inherit nixos-install nixos-generate-config nixos-option nixos-rebuild nixos-enter;
diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix
index 64a8f7846b4..820450e3ce2 100644
--- a/nixos/modules/misc/documentation.nix
+++ b/nixos/modules/misc/documentation.nix
@@ -38,6 +38,7 @@ let
           modules = [ {
             _module.check = false;
           } ] ++ docModules.eager;
+          class = "nixos";
           specialArgs = specialArgs // {
             pkgs = scrubDerivations "pkgs" pkgs;
             # allow access to arbitrary options for eager modules, eg for getting
@@ -50,7 +51,7 @@ let
           (name: value:
             let
               wholeName = "${namePrefix}.${name}";
-              guard = lib.warn "Attempt to evaluate package ${wholeName} in option documentation; this is not supported and will eventually be an error. Use `mkPackageOption` or `literalExpression` instead.";
+              guard = lib.warn "Attempt to evaluate package ${wholeName} in option documentation; this is not supported and will eventually be an error. Use `mkPackageOption{,MD}` or `literalExpression` instead.";
             in if isAttrs value then
               scrubDerivations wholeName value
               // optionalAttrs (isDerivation value) {
@@ -106,7 +107,7 @@ let
             } >&2
         '';
 
-    inherit (cfg.nixos.options) warningsAreErrors allowDocBook;
+    inherit (cfg.nixos.options) warningsAreErrors;
   };
 
 
@@ -131,7 +132,8 @@ let
     desktopItem = pkgs.makeDesktopItem {
       name = "nixos-manual";
       desktopName = "NixOS Manual";
-      genericName = "View NixOS documentation in a web browser";
+      genericName = "System Manual";
+      comment = "View NixOS documentation in a web browser";
       icon = "nix-snowflake";
       exec = "nixos-help";
       categories = ["System"];
@@ -158,6 +160,9 @@ in
     (mkRenamedOptionModule [ "programs" "info" "enable" ] [ "documentation" "info" "enable" ])
     (mkRenamedOptionModule [ "programs" "man"  "enable" ] [ "documentation" "man"  "enable" ])
     (mkRenamedOptionModule [ "services" "nixosManual" "enable" ] [ "documentation" "nixos" "enable" ])
+    (mkRemovedOptionModule
+      [ "documentation" "nixos" "options" "allowDocBook" ]
+      "DocBook option documentation is no longer supported")
   ];
 
   options = {
@@ -262,23 +267,6 @@ in
         '';
       };
 
-      nixos.options.allowDocBook = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Whether to allow DocBook option docs. When set to `false` all option using
-          DocBook documentation will cause a manual build error; additionally a new
-          renderer may be used.
-
-          ::: {.note}
-          The `false` setting for this option is not yet fully supported. While it
-          should work fine and produce the same output as the previous toolchain
-          using DocBook it may not work in all circumstances. Whether markdown option
-          documentation is allowed is independent of this option.
-          :::
-        '';
-      };
-
       nixos.options.warningsAreErrors = mkOption {
         type = types.bool;
         default = true;
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index 17ea04cb4ec..dc59ccb357d 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -69,7 +69,7 @@ in
       #dialout = 27; # unused
       polkituser = 28;
       #utmp = 29; # unused
-      # ddclient = 30; # converted to DynamicUser = true
+      # ddclient = 30; # software removed
       davfs2 = 31;
       disnix = 33;
       osgi = 34;
@@ -233,7 +233,7 @@ in
       # nix-serve = 199; # unused, removed 2020-12-12
       #tvheadend = 200; # dynamically allocated as of 2021-09-18
       uwsgi = 201;
-      gitit = 202;
+      # gitit = 202; # unused, module was removed 2023-04-03
       riemanntools = 203;
       subsonic = 204;
       # riak = 205; # unused, remove 2022-07-22
@@ -338,7 +338,7 @@ in
       lidarr = 306;
       slurm = 307;
       kapacitor = 308;
-      solr = 309;
+      # solr = 309; removed 2023-03-16
       alerta = 310;
       minetest = 311;
       rss2email = 312;
@@ -394,7 +394,7 @@ in
       dialout = 27;
       #polkituser = 28; # currently unused, polkitd doesn't need a group
       utmp = 29;
-      # ddclient = 30; # converted to DynamicUser = true
+      # ddclient = 30; # software removed
       davfs2 = 31;
       disnix = 33;
       osgi = 34;
@@ -648,7 +648,7 @@ in
       lidarr = 306;
       slurm = 307;
       kapacitor = 308;
-      solr = 309;
+      # solr = 309; removed 2023-03-16
       alerta = 310;
       minetest = 311;
       rss2email = 312;
diff --git a/nixos/modules/misc/man-db.nix b/nixos/modules/misc/man-db.nix
index 299b11d1fce..2b980561218 100644
--- a/nixos/modules/misc/man-db.nix
+++ b/nixos/modules/misc/man-db.nix
@@ -13,11 +13,21 @@ in
         example = false;
       };
 
+      skipPackages = lib.mkOption {
+        type = lib.types.listOf lib.types.package;
+        default = [];
+        internal = true;
+        description = lib.mdDoc ''
+          Packages to *not* include in the man-db.
+          This can be useful to avoid unnecessary rebuilds due to packages that change frequently, like nixos-version.
+        '';
+      };
+
       manualPages = lib.mkOption {
         type = lib.types.path;
         default = pkgs.buildEnv {
           name = "man-paths";
-          paths = config.environment.systemPackages;
+          paths = lib.subtractLists cfg.skipPackages config.environment.systemPackages;
           pathsToLink = [ "/share/man" ];
           extraOutputsToInstall = [ "man" ]
             ++ lib.optionals config.documentation.dev.enable [ "devman" ];
diff --git a/nixos/modules/misc/meta.nix b/nixos/modules/misc/meta.nix
index e1d16f802ce..95f2765aff1 100644
--- a/nixos/modules/misc/meta.nix
+++ b/nixos/modules/misc/meta.nix
@@ -47,7 +47,7 @@ in
       doc = mkOption {
         type = docFile;
         internal = true;
-        example = "./meta.chapter.xml";
+        example = "./meta.chapter.md";
         description = lib.mdDoc ''
           Documentation prologue for the set of options of each module.  This
           option should be defined at most once per module.
diff --git a/nixos/modules/misc/nixpkgs.nix b/nixos/modules/misc/nixpkgs.nix
index 7f44c3f6f3f..f9d8bccea28 100644
--- a/nixos/modules/misc/nixpkgs.nix
+++ b/nixos/modules/misc/nixpkgs.nix
@@ -49,17 +49,12 @@ let
     merge = lib.mergeOneOption;
   };
 
-  pkgsType = mkOptionType {
-    name = "nixpkgs";
+  pkgsType = types.pkgs // {
+    # This type is only used by itself, so let's elaborate the description a bit
+    # for the purpose of documentation.
     description = "An evaluation of Nixpkgs; the top level attribute set of packages";
-    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;
@@ -337,10 +332,28 @@ in
 
   config = {
     _module.args = {
-      pkgs = finalPkgs.__splicedPackages;
+      pkgs =
+        # We explicitly set the default override priority, so that we do not need
+        # to evaluate finalPkgs in case an override is placed on `_module.args.pkgs`.
+        # After all, to determine a definition priority, we need to evaluate `._type`,
+        # which is somewhat costly for Nixpkgs. With an explicit priority, we only
+        # evaluate the wrapper to find out that the priority is lower, and then we
+        # don't need to evaluate `finalPkgs`.
+        lib.mkOverride lib.modules.defaultOverridePriority
+          finalPkgs.__splicedPackages;
     };
 
-    assertions = [
+    assertions = let
+      # Whether `pkgs` was constructed by this module. This is false when any of
+      # nixpkgs.pkgs or _module.args.pkgs is set.
+      constructedByMe =
+        # We set it with default priority and it can not be merged, so if the
+        # pkgs module argument has that priority, it's from us.
+        (lib.modules.mergeAttrDefinitionsWithPrio options._module.args).pkgs.highestPrio
+          == lib.modules.defaultOverridePriority
+        # Although, if nixpkgs.pkgs is set, we did forward it, but we did not construct it.
+          && !opt.pkgs.isDefined;
+    in [
       (
         let
           nixosExpectedSystem =
diff --git a/nixos/modules/misc/nixpkgs/read-only.nix b/nixos/modules/misc/nixpkgs/read-only.nix
new file mode 100644
index 00000000000..2a783216a9d
--- /dev/null
+++ b/nixos/modules/misc/nixpkgs/read-only.nix
@@ -0,0 +1,74 @@
+# A replacement for the traditional nixpkgs module, such that none of the modules
+# can add their own configuration. This ensures that the Nixpkgs configuration is
+# exactly as the user intends.
+# This may also be used as a performance optimization when evaluating multiple
+# configurations at once, with a shared `pkgs`.
+
+# This is a separate module, because merging this logic into the nixpkgs module
+# is too burdensome, considering that it is already burdened with legacy.
+# Moving this logic into a module does not lose any composition benefits, because
+# its purpose is not something that composes anyway.
+
+{ lib, config, ... }:
+
+let
+  cfg = config.nixpkgs;
+  inherit (lib) mkOption types;
+
+in
+{
+  disabledModules = [
+    ../nixpkgs.nix
+  ];
+  options = {
+    nixpkgs = {
+      pkgs = mkOption {
+        type = lib.types.pkgs;
+        description = lib.mdDoc ''The pkgs module argument.'';
+      };
+      config = mkOption {
+        internal = true;
+        type = types.unique { message = "nixpkgs.config is set to read-only"; } types.anything;
+        description = lib.mdDoc ''
+          The Nixpkgs `config` that `pkgs` was initialized with.
+        '';
+      };
+      overlays = mkOption {
+        internal = true;
+        type = types.unique { message = "nixpkgs.overlays is set to read-only"; } types.anything;
+        description = lib.mdDoc ''
+          The Nixpkgs overlays that `pkgs` was initialized with.
+        '';
+      };
+      hostPlatform = mkOption {
+        internal = true;
+        readOnly = true;
+        description = lib.mdDoc ''
+          The platform of the machine that is running the NixOS configuration.
+        '';
+      };
+      buildPlatform = mkOption {
+        internal = true;
+        readOnly = true;
+        description = lib.mdDoc ''
+          The platform of the machine that built the NixOS configuration.
+        '';
+      };
+      # NOTE: do not add the legacy options such as localSystem here. Let's keep
+      #       this module simple and let module authors upgrade their code instead.
+    };
+  };
+  config = {
+    _module.args.pkgs =
+      # find mistaken definitions
+      builtins.seq cfg.config
+      builtins.seq cfg.overlays
+      builtins.seq cfg.hostPlatform
+      builtins.seq cfg.buildPlatform
+      cfg.pkgs;
+    nixpkgs.config = cfg.pkgs.config;
+    nixpkgs.overlays = cfg.pkgs.overlays;
+    nixpkgs.hostPlatform = cfg.pkgs.stdenv.hostPlatform;
+    nixpkgs.buildPlatform = cfg.pkgs.stdenv.buildPlatform;
+  };
+}
diff --git a/nixos/modules/misc/nixpkgs/test.nix b/nixos/modules/misc/nixpkgs/test.nix
index a6d8877ae07..0536cfc9624 100644
--- a/nixos/modules/misc/nixpkgs/test.nix
+++ b/nixos/modules/misc/nixpkgs/test.nix
@@ -1,3 +1,5 @@
+# [nixpkgs]$ nix-build -A nixosTests.nixpkgs --show-trace
+
 { evalMinimalConfig, pkgs, lib, stdenv }:
 let
   eval = mod: evalMinimalConfig {
@@ -27,6 +29,47 @@ let
     let
       uncheckedEval = lib.evalModules { modules = [ ../nixpkgs.nix module ]; };
     in map (ass: ass.message) (lib.filter (ass: !ass.assertion) uncheckedEval.config.assertions);
+
+  readOnlyUndefined = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+  };
+
+  readOnlyBad = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+    nixpkgs.pkgs = { };
+  };
+
+  readOnly = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+    nixpkgs.pkgs = pkgs;
+  };
+
+  readOnlyBadConfig = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+    nixpkgs.pkgs = pkgs;
+    nixpkgs.config.allowUnfree = true; # do in pkgs instead!
+  };
+
+  readOnlyBadOverlays = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+    nixpkgs.pkgs = pkgs;
+    nixpkgs.overlays = [ (_: _: {}) ]; # do in pkgs instead!
+  };
+
+  readOnlyBadHostPlatform = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+    nixpkgs.pkgs = pkgs;
+    nixpkgs.hostPlatform = "foo-linux"; # do in pkgs instead!
+  };
+
+  readOnlyBadBuildPlatform = evalMinimalConfig {
+    imports = [ ./read-only.nix ];
+    nixpkgs.pkgs = pkgs;
+    nixpkgs.buildPlatform = "foo-linux"; # do in pkgs instead!
+  };
+
+  throws = x: ! (builtins.tryEval x).success;
+
 in
 lib.recurseIntoAttrs {
   invokeNixpkgsSimple =
@@ -65,5 +108,21 @@ lib.recurseIntoAttrs {
         nixpkgs.pkgs = pkgs;
       } == [];
 
+
+    # Tests for the read-only.nix module
+    assert readOnly._module.args.pkgs.stdenv.hostPlatform.system == pkgs.stdenv.hostPlatform.system;
+    assert throws readOnlyBad._module.args.pkgs.stdenv;
+    assert throws readOnlyUndefined._module.args.pkgs.stdenv;
+    assert throws readOnlyBadConfig._module.args.pkgs.stdenv;
+    assert throws readOnlyBadOverlays._module.args.pkgs.stdenv;
+    assert throws readOnlyBadHostPlatform._module.args.pkgs.stdenv;
+    assert throws readOnlyBadBuildPlatform._module.args.pkgs.stdenv;
+    # read-only.nix does not provide legacy options, for the sake of simplicity
+    # If you're bothered by this, upgrade your configs to use the new *Platform
+    # options.
+    assert !readOnly.options.nixpkgs?system;
+    assert !readOnly.options.nixpkgs?localSystem;
+    assert !readOnly.options.nixpkgs?crossSystem;
+
     pkgs.emptyFile;
 }
diff --git a/nixos/modules/misc/version.nix b/nixos/modules/misc/version.nix
index 1067b21a22b..0a66eafe933 100644
--- a/nixos/modules/misc/version.nix
+++ b/nixos/modules/misc/version.nix
@@ -9,28 +9,30 @@ let
     literalExpression mkRenamedOptionModule mkDefault mkOption trivial types;
 
   needsEscaping = s: null != builtins.match "[a-zA-Z0-9]+" s;
-  escapeIfNeccessary = s: if needsEscaping s then s else ''"${lib.escape [ "\$" "\"" "\\" "\`" ] s}"'';
+  escapeIfNecessary = s: if needsEscaping s then s else ''"${lib.escape [ "\$" "\"" "\\" "\`" ] s}"'';
   attrsToText = attrs:
     concatStringsSep "\n" (
-      mapAttrsToList (n: v: ''${n}=${escapeIfNeccessary (toString v)}'') attrs
+      mapAttrsToList (n: v: ''${n}=${escapeIfNecessary (toString v)}'') attrs
     ) + "\n";
 
   osReleaseContents = {
-    NAME = "NixOS";
-    ID = "nixos";
+    NAME = "${cfg.distroName}";
+    ID = "${cfg.distroId}";
     VERSION = "${cfg.release} (${cfg.codeName})";
     VERSION_CODENAME = toLower cfg.codeName;
     VERSION_ID = cfg.release;
     BUILD_ID = cfg.version;
-    PRETTY_NAME = "NixOS ${cfg.release} (${cfg.codeName})";
+    PRETTY_NAME = "${cfg.distroName} ${cfg.release} (${cfg.codeName})";
     LOGO = "nix-snowflake";
-    HOME_URL = "https://nixos.org/";
-    DOCUMENTATION_URL = "https://nixos.org/learn.html";
-    SUPPORT_URL = "https://nixos.org/community.html";
-    BUG_REPORT_URL = "https://github.com/NixOS/nixpkgs/issues";
+    HOME_URL = lib.optionalString (cfg.distroId == "nixos") "https://nixos.org/";
+    DOCUMENTATION_URL = lib.optionalString (cfg.distroId == "nixos") "https://nixos.org/learn.html";
+    SUPPORT_URL = lib.optionalString (cfg.distroId == "nixos") "https://nixos.org/community.html";
+    BUG_REPORT_URL = lib.optionalString (cfg.distroId == "nixos") "https://github.com/NixOS/nixpkgs/issues";
+  } // lib.optionalAttrs (cfg.variant_id != null) {
+    VARIANT_ID = cfg.variant_id;
   };
 
-  initrdReleaseContents = osReleaseContents // {
+  initrdReleaseContents = (removeAttrs osReleaseContents [ "BUILD_ID" ]) // {
     PRETTY_NAME = "${osReleaseContents.PRETTY_NAME} (Initrd)";
   };
   initrdRelease = pkgs.writeText "initrd-release" (attrsToText initrdReleaseContents);
@@ -87,6 +89,27 @@ in
       description = lib.mdDoc "The NixOS release code name (e.g. `Emu`).";
     };
 
+    nixos.distroId = mkOption {
+      internal = true;
+      type = types.str;
+      default = "nixos";
+      description = lib.mdDoc "The id of the operating system";
+    };
+
+    nixos.distroName = mkOption {
+      internal = true;
+      type = types.str;
+      default = "NixOS";
+      description = lib.mdDoc "The name of the operating system";
+    };
+
+    nixos.variant_id = mkOption {
+      type = types.nullOr (types.strMatching "^[a-z0-9._-]+$");
+      default = null;
+      description = lib.mdDoc "A lower-case string identifying a specific variant or edition of the operating system";
+      example = "installer";
+    };
+
     stateVersion = mkOption {
       type = types.str;
       # TODO Remove this and drop the default of the option so people are forced to set it.
@@ -107,7 +130,7 @@ in
         to be compatible. The effect is that NixOS will use
         defaults corresponding to the specified release (such as using
         an older version of PostgreSQL).
-        It‘s perfectly fine and recommended to leave this value at the
+        It’s perfectly fine and recommended to leave this value at the
         release version of the first install of this system.
         Changing this option will not upgrade your system. In fact it
         is meant to stay constant exactly when you upgrade your system.
@@ -117,13 +140,6 @@ in
       '';
     };
 
-    defaultChannel = mkOption {
-      internal = true;
-      type = types.str;
-      default = "https://nixos.org/channels/nixos-unstable";
-      description = lib.mdDoc "Default NixOS channel to which the root user is subscribed.";
-    };
-
     configurationRevision = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -146,10 +162,10 @@ in
     environment.etc = {
       "lsb-release".text = attrsToText {
         LSB_VERSION = "${cfg.release} (${cfg.codeName})";
-        DISTRIB_ID = "nixos";
+        DISTRIB_ID = "${cfg.distroId}";
         DISTRIB_RELEASE = cfg.release;
         DISTRIB_CODENAME = toLower cfg.codeName;
-        DISTRIB_DESCRIPTION = "NixOS ${cfg.release} (${cfg.codeName})";
+        DISTRIB_DESCRIPTION = "${cfg.distroName} ${cfg.release} (${cfg.codeName})";
       };
 
       "os-release".text = attrsToText osReleaseContents;
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 41b953dc347..73242f22b0d 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -16,13 +16,18 @@
   ./config/malloc.nix
   ./config/mysql.nix
   ./config/networking.nix
+  ./config/nix.nix
+  ./config/nix-channel.nix
+  ./config/nix-flakes.nix
+  ./config/nix-remote-build.nix
   ./config/no-x-libs.nix
   ./config/nsswitch.nix
   ./config/power-management.nix
   ./config/pulseaudio.nix
-  ./config/qt5.nix
+  ./config/qt.nix
   ./config/resolvconf.nix
   ./config/shells-environment.nix
+  ./config/stevenblack.nix
   ./config/swap.nix
   ./config/sysctl.nix
   ./config/system-environment.nix
@@ -52,11 +57,13 @@
   ./hardware/cpu/intel-sgx.nix
   ./hardware/device-tree.nix
   ./hardware/digitalbitbox.nix
+  ./hardware/flipperzero.nix
   ./hardware/flirc.nix
   ./hardware/gkraken.nix
   ./hardware/gpgsmartcards.nix
   ./hardware/hackrf.nix
   ./hardware/i2c.nix
+  ./hardware/keyboard/qmk.nix
   ./hardware/keyboard/teck.nix
   ./hardware/keyboard/uhk.nix
   ./hardware/keyboard/zsa.nix
@@ -92,16 +99,15 @@
   ./hardware/video/bumblebee.nix
   ./hardware/video/capture/mwprocapture.nix
   ./hardware/video/displaylink.nix
-  ./hardware/video/hidpi.nix
   ./hardware/video/nvidia.nix
   ./hardware/video/switcheroo-control.nix
   ./hardware/video/uvcvideo/default.nix
   ./hardware/video/webcam/facetimehd.nix
+  ./hardware/video/webcam/ipu6.nix
   ./hardware/wooting.nix
   ./hardware/xone.nix
   ./hardware/xpadneo.nix
   ./i18n/input-method/default.nix
-  ./i18n/input-method/fcitx.nix
   ./i18n/input-method/fcitx5.nix
   ./i18n/input-method/hime.nix
   ./i18n/input-method/ibus.nix
@@ -147,9 +153,11 @@
   ./programs/cdemu.nix
   ./programs/cfs-zen-tweaks.nix
   ./programs/chromium.nix
+  ./programs/clash-verge.nix
   ./programs/cnping.nix
   ./programs/command-not-found/command-not-found.nix
   ./programs/criu.nix
+  ./programs/darling.nix
   ./programs/dconf.nix
   ./programs/digitalbitbox/default.nix
   ./programs/dmrconfig.nix
@@ -168,10 +176,10 @@
   ./programs/fuse.nix
   ./programs/fzf.nix
   ./programs/gamemode.nix
+  ./programs/gamescope.nix
   ./programs/geary.nix
   ./programs/git.nix
   ./programs/gnome-disks.nix
-  ./programs/gnome-documents.nix
   ./programs/gnome-terminal.nix
   ./programs/gnupg.nix
   ./programs/gpaste.nix
@@ -179,6 +187,8 @@
   ./programs/haguichi.nix
   ./programs/hamster.nix
   ./programs/htop.nix
+  ./programs/hyprland.nix
+  ./programs/iay.nix
   ./programs/iftop.nix
   ./programs/i3lock.nix
   ./programs/iotop.nix
@@ -194,6 +204,8 @@
   ./programs/mdevctl.nix
   ./programs/mepo.nix
   ./programs/mininet.nix
+  ./programs/minipro.nix
+  ./programs/miriway.nix
   ./programs/mosh.nix
   ./programs/msmtp.nix
   ./programs/mtr.nix
@@ -201,6 +213,7 @@
   ./programs/nbd.nix
   ./programs/neovim.nix
   ./programs/nethoscope.nix
+  ./programs/nexttrace.nix
   ./programs/nix-index.nix
   ./programs/nix-ld.nix
   ./programs/nm-applet.nix
@@ -213,22 +226,25 @@
   ./programs/partition-manager.nix
   ./programs/plotinus.nix
   ./programs/proxychains.nix
+  ./programs/qdmr.nix
   ./programs/qt5ct.nix
+  ./programs/regreet.nix
   ./programs/rog-control-center.nix
   ./programs/rust-motd.nix
   ./programs/screen.nix
   ./programs/seahorse.nix
   ./programs/sedutil.nix
   ./programs/shadow.nix
+  ./programs/sharing.nix
   ./programs/singularity.nix
   ./programs/skim.nix
   ./programs/slock.nix
+  ./programs/sniffnet.nix
   ./programs/spacefm.nix
   ./programs/ssh.nix
   ./programs/starship.nix
   ./programs/steam.nix
   ./programs/streamdeck-ui.nix
-  ./programs/sway.nix
   ./programs/sysdig.nix
   ./programs/system-config-printer.nix
   ./programs/systemtap.nix
@@ -236,13 +252,16 @@
   ./programs/thunar.nix
   ./programs/tmux.nix
   ./programs/traceroute.nix
+  ./programs/trippy.nix
   ./programs/tsm-client.nix
   ./programs/turbovnc.nix
   ./programs/udevil.nix
   ./programs/usbtop.nix
   ./programs/vim.nix
   ./programs/wavemon.nix
-  ./programs/waybar.nix
+  ./programs/wayland/river.nix
+  ./programs/wayland/sway.nix
+  ./programs/wayland/waybar.nix
   ./programs/weylus.nix
   ./programs/wireshark.nix
   ./programs/xastir.nix
@@ -270,6 +289,7 @@
   ./security/doas.nix
   ./security/duosec.nix
   ./security/google_oslogin.nix
+  ./security/ipa.nix
   ./security/lock-kernel-modules.nix
   ./security/misc.nix
   ./security/oath.nix
@@ -293,6 +313,8 @@
   ./services/amqp/rabbitmq.nix
   ./services/audio/alsa.nix
   ./services/audio/botamusique.nix
+  ./services/audio/gmediarender.nix
+  ./services/audio/gonic.nix
   ./services/audio/hqplayerd.nix
   ./services/audio/icecast.nix
   ./services/audio/jack.nix
@@ -309,6 +331,9 @@
   ./services/audio/snapserver.nix
   ./services/audio/spotifyd.nix
   ./services/audio/squeezelite.nix
+  ./services/audio/tts.nix
+  ./services/audio/wyoming/faster-whisper.nix
+  ./services/audio/wyoming/piper.nix
   ./services/audio/ympd.nix
   ./services/backup/automysqlbackup.nix
   ./services/backup/bacula.nix
@@ -357,6 +382,7 @@
   ./services/continuous-integration/buildbot/master.nix
   ./services/continuous-integration/buildbot/worker.nix
   ./services/continuous-integration/buildkite-agents.nix
+  ./services/continuous-integration/gitea-actions-runner.nix
   ./services/continuous-integration/github-runner.nix
   ./services/continuous-integration/github-runners.nix
   ./services/continuous-integration/gitlab-runner.nix
@@ -368,6 +394,8 @@
   ./services/continuous-integration/jenkins/default.nix
   ./services/continuous-integration/jenkins/job-builder.nix
   ./services/continuous-integration/jenkins/slave.nix
+  ./services/continuous-integration/woodpecker/agents.nix
+  ./services/continuous-integration/woodpecker/server.nix
   ./services/databases/aerospike.nix
   ./services/databases/cassandra.nix
   ./services/databases/clickhouse.nix
@@ -380,6 +408,7 @@
   ./services/databases/hbase-standalone.nix
   ./services/databases/influxdb.nix
   ./services/databases/influxdb2.nix
+  ./services/databases/lldap.nix
   ./services/databases/memcached.nix
   ./services/databases/monetdb.nix
   ./services/databases/mongodb.nix
@@ -396,6 +425,9 @@
   ./services/desktops/bamf.nix
   ./services/desktops/blueman.nix
   ./services/desktops/cpupower-gui.nix
+  ./services/desktops/deepin/dde-api.nix
+  ./services/desktops/deepin/app-services.nix
+  ./services/desktops/deepin/dde-daemon.nix
   ./services/desktops/dleyna-renderer.nix
   ./services/desktops/dleyna-server.nix
   ./services/desktops/espanso.nix
@@ -420,17 +452,18 @@
   ./services/desktops/gvfs.nix
   ./services/desktops/malcontent.nix
   ./services/desktops/neard.nix
-  ./services/desktops/pipewire/pipewire-media-session.nix
   ./services/desktops/pipewire/pipewire.nix
   ./services/desktops/pipewire/wireplumber.nix
   ./services/desktops/profile-sync-daemon.nix
   ./services/desktops/system-config-printer.nix
+  ./services/desktops/system76-scheduler.nix
   ./services/desktops/telepathy.nix
   ./services/desktops/tumbler.nix
   ./services/desktops/zeitgeist.nix
   ./services/development/blackfire.nix
   ./services/development/bloop.nix
   ./services/development/distccd.nix
+  ./services/development/gemstash.nix
   ./services/development/hoogle.nix
   ./services/development/jupyter/default.nix
   ./services/development/jupyterhub/default.nix
@@ -447,6 +480,7 @@
   ./services/games/deliantra-server.nix
   ./services/games/factorio.nix
   ./services/games/freeciv.nix
+  ./services/games/mchprs.nix
   ./services/games/minecraft-server.nix
   ./services/games/minetest-server.nix
   ./services/games/openarena.nix
@@ -499,6 +533,8 @@
   ./services/hardware/usbmuxd.nix
   ./services/hardware/usbrelayd.nix
   ./services/hardware/vdr.nix
+  ./services/hardware/keyd.nix
+  ./services/home-automation/esphome.nix
   ./services/home-automation/evcc.nix
   ./services/home-automation/home-assistant.nix
   ./services/home-automation/zigbee2mqtt.nix
@@ -527,6 +563,7 @@
   ./services/mail/dovecot.nix
   ./services/mail/dspam.nix
   ./services/mail/exim.nix
+  ./services/mail/goeland.nix
   ./services/mail/listmonk.nix
   ./services/mail/maddy.nix
   ./services/mail/mail.nix
@@ -550,6 +587,7 @@
   ./services/mail/schleuder.nix
   ./services/mail/spamassassin.nix
   ./services/mail/sympa.nix
+  ./services/mail/zeyple.nix
   ./services/matrix/appservice-discord.nix
   ./services/matrix/appservice-irc.nix
   ./services/matrix/conduit.nix
@@ -557,6 +595,7 @@
   ./services/matrix/mautrix-facebook.nix
   ./services/matrix/mautrix-telegram.nix
   ./services/matrix/mjolnir.nix
+  ./services/matrix/mx-puppet-discord.nix
   ./services/matrix/pantalaimon.nix
   ./services/matrix/synapse.nix
   ./services/misc/airsonic.nix
@@ -566,6 +605,7 @@
   ./services/misc/atuin.nix
   ./services/misc/autofs.nix
   ./services/misc/autorandr.nix
+  ./services/misc/autosuspend.nix
   ./services/misc/bazarr.nix
   ./services/misc/beanstalkd.nix
   ./services/misc/bees.nix
@@ -590,14 +630,12 @@
   ./services/misc/etcd.nix
   ./services/misc/etebase-server.nix
   ./services/misc/etesync-dav.nix
-  ./services/misc/exhibitor.nix
   ./services/misc/felix.nix
   ./services/misc/freeswitch.nix
   ./services/misc/fstrim.nix
   ./services/misc/gammu-smsd.nix
   ./services/misc/geoipupdate.nix
   ./services/misc/gitea.nix
-  # ./services/misc/gitit.nix
   ./services/misc/gitlab.nix
   ./services/misc/gitolite.nix
   ./services/misc/gitweb.nix
@@ -612,6 +650,7 @@
   ./services/misc/irkerd.nix
   ./services/misc/jackett.nix
   ./services/misc/jellyfin.nix
+  ./services/misc/jellyseerr.nix
   ./services/misc/klipper.nix
   ./services/misc/languagetool.nix
   ./services/misc/leaps.nix
@@ -624,10 +663,8 @@
   ./services/misc/mediatomb.nix
   ./services/misc/metabase.nix
   ./services/misc/moonraker.nix
-  ./services/misc/mx-puppet-discord.nix
   ./services/misc/n8n.nix
   ./services/misc/nitter.nix
-  ./services/misc/nix-daemon.nix
   ./services/misc/nix-gc.nix
   ./services/misc/nix-optimise.nix
   ./services/misc/nix-ssh-serve.nix
@@ -650,12 +687,15 @@
   ./services/misc/polaris.nix
   ./services/misc/portunus.nix
   ./services/misc/prowlarr.nix
+  ./services/misc/pufferpanel.nix
   ./services/misc/pykms.nix
   ./services/misc/radarr.nix
+  ./services/misc/readarr.nix
   ./services/misc/redmine.nix
   ./services/misc/ripple-data-api.nix
   ./services/misc/rippled.nix
   ./services/misc/rmfakecloud.nix
+  ./services/misc/rshim.nix
   ./services/misc/safeeyes.nix
   ./services/misc/sdrplay.nix
   ./services/misc/serviio.nix
@@ -689,8 +729,10 @@
   ./services/monitoring/alerta.nix
   ./services/monitoring/apcupsd.nix
   ./services/monitoring/arbtt.nix
+  ./services/monitoring/below.nix
   ./services/monitoring/bosun.nix
   ./services/monitoring/cadvisor.nix
+  ./services/monitoring/cockpit.nix
   ./services/monitoring/collectd.nix
   ./services/monitoring/das_watchdog.nix
   ./services/monitoring/datadog-agent.nix
@@ -716,7 +758,9 @@
   ./services/monitoring/munin.nix
   ./services/monitoring/nagios.nix
   ./services/monitoring/netdata.nix
+  ./services/monitoring/opentelemetry-collector.nix
   ./services/monitoring/parsedmarc.nix
+  ./services/monitoring/prometheus/alertmanager-irc-relay.nix
   ./services/monitoring/prometheus/alertmanager.nix
   ./services/monitoring/prometheus/default.nix
   ./services/monitoring/prometheus/exporters.nix
@@ -740,6 +784,7 @@
   ./services/monitoring/uptime-kuma.nix
   ./services/monitoring/uptime.nix
   ./services/monitoring/vmagent.nix
+  ./services/monitoring/vmalert.nix
   ./services/monitoring/vnstat.nix
   ./services/monitoring/zabbix-agent.nix
   ./services/monitoring/zabbix-proxy.nix
@@ -770,7 +815,9 @@
   ./services/network-filesystems/xtreemfs.nix
   ./services/network-filesystems/yandex-disk.nix
   ./services/networking/3proxy.nix
+  ./services/networking/acme-dns.nix
   ./services/networking/adguardhome.nix
+  ./services/networking/alice-lg.nix
   ./services/networking/amuled.nix
   ./services/networking/antennas.nix
   ./services/networking/aria2.nix
@@ -785,10 +832,12 @@
   ./services/networking/bind.nix
   ./services/networking/bird-lg.nix
   ./services/networking/bird.nix
+  ./services/networking/birdwatcher.nix
   ./services/networking/bitcoind.nix
   ./services/networking/bitlbee.nix
   ./services/networking/blockbook-frontend.nix
   ./services/networking/blocky.nix
+  ./services/networking/cgit.nix
   ./services/networking/charybdis.nix
   ./services/networking/chisel-server.nix
   ./services/networking/cjdns.nix
@@ -803,7 +852,6 @@
   ./services/networking/create_ap.nix
   ./services/networking/croc.nix
   ./services/networking/dante.nix
-  ./services/networking/ddclient.nix
   ./services/networking/dhcpcd.nix
   ./services/networking/dhcpd.nix
   ./services/networking/dnscache.nix
@@ -842,6 +890,7 @@
   ./services/networking/gobgpd.nix
   ./services/networking/gvpe.nix
   ./services/networking/hans.nix
+  ./services/networking/harmonia.nix
   ./services/networking/haproxy.nix
   ./services/networking/headscale.nix
   ./services/networking/hostapd.nix
@@ -852,6 +901,7 @@
   ./services/networking/i2pd.nix
   ./services/networking/icecream/daemon.nix
   ./services/networking/icecream/scheduler.nix
+  ./services/networking/imaginary.nix
   ./services/networking/inspircd.nix
   ./services/networking/iodine.nix
   ./services/networking/iperf3.nix
@@ -859,6 +909,7 @@
   ./services/networking/iscsi/initiator.nix
   ./services/networking/iscsi/root-initiator.nix
   ./services/networking/iscsi/target.nix
+  ./services/networking/ivpn.nix
   ./services/networking/iwd.nix
   ./services/networking/jibri/default.nix
   ./services/networking/jicofo.nix
@@ -869,6 +920,7 @@
   ./services/networking/knot.nix
   ./services/networking/kresd.nix
   ./services/networking/lambdabot.nix
+  ./services/networking/legit.nix
   ./services/networking/libreswan.nix
   ./services/networking/lldpd.nix
   ./services/networking/logmein-hamachi.nix
@@ -903,6 +955,7 @@
   ./services/networking/ndppd.nix
   ./services/networking/nebula.nix
   ./services/networking/netbird.nix
+  ./services/networking/networkd-dispatcher.nix
   ./services/networking/networkmanager.nix
   ./services/networking/nextdns.nix
   ./services/networking/nftables.nix
@@ -930,6 +983,8 @@
   ./services/networking/owamp.nix
   ./services/networking/pdns-recursor.nix
   ./services/networking/pdnsd.nix
+  ./services/networking/peroxide.nix
+  ./services/networking/picosnitch.nix
   ./services/networking/pixiecore.nix
   ./services/networking/pleroma.nix
   ./services/networking/polipo.nix
@@ -961,6 +1016,8 @@
   ./services/networking/shorewall.nix
   ./services/networking/shorewall6.nix
   ./services/networking/shout.nix
+  ./services/networking/sing-box.nix
+  ./services/networking/sitespeed-io.nix
   ./services/networking/skydns.nix
   ./services/networking/smartdns.nix
   ./services/networking/smokeping.nix
@@ -1015,8 +1072,10 @@
   ./services/networking/wg-netmanager.nix
   ./services/networking/webhook.nix
   ./services/networking/wg-quick.nix
+  ./services/networking/wgautomesh.nix
   ./services/networking/wireguard.nix
   ./services/networking/wpa_supplicant.nix
+  ./services/networking/wstunnel.nix
   ./services/networking/x2goserver.nix
   ./services/networking/xandikos.nix
   ./services/networking/xinetd.nix
@@ -1039,8 +1098,10 @@
   ./services/search/hound.nix
   ./services/search/kibana.nix
   ./services/search/meilisearch.nix
-  ./services/search/solr.nix
+  ./services/search/opensearch.nix
+  ./services/search/qdrant.nix
   ./services/security/aesmd.nix
+  ./services/security/authelia.nix
   ./services/security/certmgr.nix
   ./services/security/cfssl.nix
   ./services/security/clamav.nix
@@ -1073,6 +1134,7 @@
   ./services/security/torsocks.nix
   ./services/security/usbguard.nix
   ./services/security/vault.nix
+  ./services/security/vault-agent.nix
   ./services/security/vaultwarden/default.nix
   ./services/security/yubikey-agent.nix
   ./services/system/automatic-timezoned.nix
@@ -1083,6 +1145,7 @@
   ./services/system/earlyoom.nix
   ./services/system/kerberos/default.nix
   ./services/system/localtimed.nix
+  ./services/system/nix-daemon.nix
   ./services/system/nscd.nix
   ./services/system/saslauthd.nix
   ./services/system/self-deploy.nix
@@ -1100,20 +1163,26 @@
   ./services/ttys/gpm.nix
   ./services/ttys/kmscon.nix
   ./services/video/epgstation/default.nix
+  ./services/video/go2rtc/default.nix
+  ./services/video/frigate.nix
   ./services/video/mirakurun.nix
   ./services/video/replay-sorcery.nix
-  ./services/video/rtsp-simple-server.nix
+  ./services/video/mediamtx.nix
   ./services/video/unifi-video.nix
+  ./services/video/v4l2-relayd.nix
   ./services/wayland/cage.nix
   ./services/web-apps/akkoma.nix
   ./services/web-apps/alps.nix
+  ./services/web-apps/anuko-time-tracker.nix
   ./services/web-apps/atlassian/confluence.nix
   ./services/web-apps/atlassian/crowd.nix
   ./services/web-apps/atlassian/jira.nix
-  ./services/web-apps/baget.nix
   ./services/web-apps/bookstack.nix
   ./services/web-apps/calibre-web.nix
+  ./services/web-apps/coder.nix
   ./services/web-apps/changedetection-io.nix
+  ./services/web-apps/chatgpt-retrieval-plugin.nix
+  ./services/web-apps/cloudlog.nix
   ./services/web-apps/code-server.nix
   ./services/web-apps/convos.nix
   ./services/web-apps/dex.nix
@@ -1128,46 +1197,57 @@
   ./services/web-apps/galene.nix
   ./services/web-apps/gerrit.nix
   ./services/web-apps/gotify-server.nix
+  ./services/web-apps/gotosocial.nix
   ./services/web-apps/grocy.nix
+  ./services/web-apps/pixelfed.nix
+  ./services/web-apps/guacamole-client.nix
+  ./services/web-apps/guacamole-server.nix
   ./services/web-apps/healthchecks.nix
   ./services/web-apps/hedgedoc.nix
   ./services/web-apps/hledger-web.nix
   ./services/web-apps/icingaweb2/icingaweb2.nix
   ./services/web-apps/icingaweb2/module-monitoring.nix
-  ./services/web-apps/ihatemoney
   ./services/web-apps/invidious.nix
   ./services/web-apps/invoiceplane.nix
   ./services/web-apps/isso.nix
   ./services/web-apps/jirafeau.nix
   ./services/web-apps/jitsi-meet.nix
+  ./services/web-apps/kasmweb/default.nix
+  ./services/web-apps/kavita.nix
   ./services/web-apps/keycloak.nix
   ./services/web-apps/komga.nix
   ./services/web-apps/lemmy.nix
   ./services/web-apps/limesurvey.nix
+  ./services/web-apps/mainsail.nix
   ./services/web-apps/mastodon.nix
   ./services/web-apps/matomo.nix
   ./services/web-apps/mattermost.nix
   ./services/web-apps/mediawiki.nix
   ./services/web-apps/miniflux.nix
+  ./services/web-apps/monica.nix
   ./services/web-apps/moodle.nix
   ./services/web-apps/netbox.nix
   ./services/web-apps/nextcloud.nix
+  ./services/web-apps/nextcloud-notify_push.nix
   ./services/web-apps/nexus.nix
   ./services/web-apps/nifi.nix
   ./services/web-apps/node-red.nix
   ./services/web-apps/onlyoffice.nix
+  ./services/web-apps/openvscode-server.nix
   ./services/web-apps/openwebrx.nix
   ./services/web-apps/outline.nix
   ./services/web-apps/peering-manager.nix
   ./services/web-apps/peertube.nix
   ./services/web-apps/pgpkeyserver-lite.nix
   ./services/web-apps/phylactery.nix
+  ./services/web-apps/photoprism.nix
   ./services/web-apps/pict-rs.nix
   ./services/web-apps/plantuml-server.nix
   ./services/web-apps/plausible.nix
   ./services/web-apps/powerdns-admin.nix
   ./services/web-apps/prosody-filer.nix
   ./services/web-apps/restya-board.nix
+  ./services/web-apps/sftpgo.nix
   ./services/web-apps/rss-bridge.nix
   ./services/web-apps/selfoss.nix
   ./services/web-apps/shiori.nix
@@ -1204,6 +1284,9 @@
   ./services/web-servers/nginx/gitweb.nix
   ./services/web-servers/phpfpm/default.nix
   ./services/web-servers/pomerium.nix
+  ./services/web-servers/rustus.nix
+  ./services/web-servers/stargazer.nix
+  ./services/web-servers/static-web-server.nix
   ./services/web-servers/tomcat.nix
   ./services/web-servers/traefik.nix
   ./services/web-servers/trafficserver/default.nix
@@ -1245,9 +1328,9 @@
   ./services/x11/window-managers/default.nix
   ./services/x11/window-managers/fluxbox.nix
   ./services/x11/window-managers/icewm.nix
-  ./services/x11/window-managers/bspwm.nix
   ./services/x11/window-managers/katriawm.nix
   ./services/x11/window-managers/metacity.nix
+  ./services/x11/window-managers/nimdow.nix
   ./services/x11/window-managers/none.nix
   ./services/x11/window-managers/twm.nix
   ./services/x11/window-managers/windowlab.nix
@@ -1257,6 +1340,7 @@
   ./services/x11/xbanish.nix
   ./services/x11/xfs.nix
   ./services/x11/xserver.nix
+  ./system/activation/activatable-system.nix
   ./system/activation/activation-script.nix
   ./system/activation/specialisation.nix
   ./system/activation/bootspec.nix
@@ -1281,6 +1365,7 @@
   ./system/boot/loader/raspberrypi/raspberrypi.nix
   ./system/boot/loader/systemd-boot/systemd-boot.nix
   ./system/boot/luksroot.nix
+  ./system/boot/stratisroot.nix
   ./system/boot/modprobe.nix
   ./system/boot/networkd.nix
   ./system/boot/plymouth.nix
@@ -1296,9 +1381,12 @@
   ./system/boot/systemd/logind.nix
   ./system/boot/systemd/nspawn.nix
   ./system/boot/systemd/oomd.nix
+  ./system/boot/systemd/repart.nix
   ./system/boot/systemd/shutdown.nix
   ./system/boot/systemd/tmpfiles.nix
   ./system/boot/systemd/user.nix
+  ./system/boot/systemd/userdbd.nix
+  ./system/boot/systemd/homed.nix
   ./system/boot/timesyncd.nix
   ./system/boot/tmp.nix
   ./system/boot/uvesafb.nix
@@ -1314,6 +1402,7 @@
   ./tasks/filesystems/cifs.nix
   ./tasks/filesystems/ecryptfs.nix
   ./tasks/filesystems/envfs.nix
+  ./tasks/filesystems/erofs.nix
   ./tasks/filesystems/exfat.nix
   ./tasks/filesystems/ext.nix
   ./tasks/filesystems/f2fs.nix
@@ -1321,6 +1410,7 @@
   ./tasks/filesystems/nfs.nix
   ./tasks/filesystems/ntfs.nix
   ./tasks/filesystems/reiserfs.nix
+  ./tasks/filesystems/squashfs.nix
   ./tasks/filesystems/unionfs-fuse.nix
   ./tasks/filesystems/vboxsf.nix
   ./tasks/filesystems/vfat.nix
@@ -1354,6 +1444,7 @@
   ./virtualisation/lxc.nix
   ./virtualisation/lxcfs.nix
   ./virtualisation/lxd.nix
+  ./virtualisation/multipass.nix
   ./virtualisation/nixos-containers.nix
   ./virtualisation/oci-containers.nix
   ./virtualisation/openstack-options.nix
diff --git a/nixos/modules/profiles/base.nix b/nixos/modules/profiles/base.nix
index 616b2470dcb..9f32f85a61e 100644
--- a/nixos/modules/profiles/base.nix
+++ b/nixos/modules/profiles/base.nix
@@ -1,5 +1,5 @@
 # This module defines the software packages included in the "minimal"
-# installation CD.  It might be useful elsewhere.
+# installation CD. It might be useful elsewhere.
 
 { config, lib, pkgs, ... }:
 
@@ -17,7 +17,6 @@
     pkgs.ddrescue
     pkgs.ccrypt
     pkgs.cryptsetup # needed for dm-crypt volumes
-    pkgs.mkpasswd # for generating password files
 
     # Some text editors.
     (pkgs.vim.customize {
@@ -32,9 +31,9 @@
     pkgs.fuse
     pkgs.fuse3
     pkgs.sshfs-fuse
-    pkgs.rsync
     pkgs.socat
     pkgs.screen
+    pkgs.tcpdump
 
     # Hardware-related tools.
     pkgs.sdparm
@@ -44,22 +43,14 @@
     pkgs.usbutils
     pkgs.nvme-cli
 
-    # Tools to create / manipulate filesystems.
-    pkgs.ntfsprogs # for resizing NTFS partitions
-    pkgs.dosfstools
-    pkgs.mtools
-    pkgs.xfsprogs.bin
-    pkgs.jfsutils
-    pkgs.f2fs-tools
-
     # Some compression/archiver tools.
     pkgs.unzip
     pkgs.zip
   ];
 
-  # Include support for various filesystems.
+  # Include support for various filesystems and tools to create / manipulate them.
   boot.supportedFilesystems =
-    [ "btrfs" "reiserfs" "vfat" "f2fs" "xfs" "ntfs" "cifs" ] ++
+    [ "btrfs" "cifs" "f2fs" "jfs" "ntfs" "reiserfs" "vfat" "xfs" ] ++
     lib.optional (lib.meta.availableOn pkgs.stdenv.hostPlatform config.boot.zfs.package) "zfs";
 
   # Configure host id for ZFS to work
diff --git a/nixos/modules/profiles/headless.nix b/nixos/modules/profiles/headless.nix
index c17cb287b72..eb29f3d6510 100644
--- a/nixos/modules/profiles/headless.nix
+++ b/nixos/modules/profiles/headless.nix
@@ -6,8 +6,6 @@
 with lib;
 
 {
-  boot.vesa = false;
-
   # Don't start a tty on the serial consoles.
   systemd.services."serial-getty@ttyS0".enable = lib.mkDefault false;
   systemd.services."serial-getty@hvc0".enable = false;
@@ -15,7 +13,7 @@ with lib;
   systemd.services."autovt@".enable = false;
 
   # Since we can't manually respond to a panic, just reboot.
-  boot.kernelParams = [ "panic=1" "boot.panic_on_fail" ];
+  boot.kernelParams = [ "panic=1" "boot.panic_on_fail" "vga=0x317" "nomodeset" ];
 
   # Don't allow emergency mode, because we don't have a console.
   systemd.enableEmergencyMode = false;
diff --git a/nixos/modules/profiles/installation-device.nix b/nixos/modules/profiles/installation-device.nix
index ae9be08c8d8..32884f4b875 100644
--- a/nixos/modules/profiles/installation-device.nix
+++ b/nixos/modules/profiles/installation-device.nix
@@ -20,6 +20,7 @@ with lib;
     ];
 
   config = {
+    system.nixos.variant_id = lib.mkDefault "installer";
 
     # Enable in installer, even if the minimal profile disables it.
     documentation.enable = mkImageMediaOverride true;
@@ -51,9 +52,9 @@ with lib;
     services.getty.helpLine = ''
       The "nixos" and "root" accounts have empty passwords.
 
-      An ssh daemon is running. You then must set a password
-      for either "root" or "nixos" with `passwd` or add an ssh key
-      to /home/nixos/.ssh/authorized_keys be able to login.
+      To log in over ssh you must set a password for either "nixos" or "root"
+      with `passwd` (prefix with `sudo` for "root"), or add your public key to
+      /home/nixos/.ssh/authorized_keys or /root/.ssh/authorized_keys.
 
       If you need a wireless connection, type
       `sudo systemctl start wpa_supplicant` and configure a
@@ -64,14 +65,14 @@ with lib;
       start the graphical user interface.
     '';
 
-    # We run sshd by default. Login via root is only possible after adding a
-    # password via "passwd" or by adding a ssh key to /home/nixos/.ssh/authorized_keys.
+    # We run sshd by default. Login is only possible after adding a
+    # password via "passwd" or by adding a ssh key to ~/.ssh/authorized_keys.
     # The latter one is particular useful if keys are manually added to
     # installation device for head-less systems i.e. arm boards by manually
     # mounting the storage in a different system.
     services.openssh = {
       enable = true;
-      permitRootLogin = "yes";
+      settings.PermitRootLogin = "yes";
     };
 
     # Enable wpa_supplicant, but don't start it by default.
diff --git a/nixos/modules/profiles/macos-builder.nix b/nixos/modules/profiles/macos-builder.nix
index fddf19ad125..83a84995618 100644
--- a/nixos/modules/profiles/macos-builder.nix
+++ b/nixos/modules/profiles/macos-builder.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, ... }:
 
 let
   keysDirectory = "/var/keys";
@@ -7,6 +7,8 @@ let
 
   keyType = "ed25519";
 
+  cfg = config.virtualisation.darwin-builder;
+
 in
 
 {
@@ -24,152 +26,218 @@ in
     }
   ];
 
-  # The builder is not intended to be used interactively
-  documentation.enable = false;
-
-  environment.etc = {
-    "ssh/ssh_host_ed25519_key" = {
-      mode = "0600";
-
-      source = ./keys/ssh_host_ed25519_key;
+  options.virtualisation.darwin-builder = with lib; {
+    diskSize = mkOption {
+      default = 20 * 1024;
+      type = types.int;
+      example = 30720;
+      description = "The maximum disk space allocated to the runner in MB";
     };
-
-    "ssh/ssh_host_ed25519_key.pub" = {
-      mode = "0644";
-
-      source = ./keys/ssh_host_ed25519_key.pub;
+    memorySize = mkOption {
+      default = 3 * 1024;
+      type = types.int;
+      example = 8192;
+      description = "The runner's memory in MB";
+    };
+    min-free = mkOption {
+      default = 1024 * 1024 * 1024;
+      type = types.int;
+      example = 1073741824;
+      description = ''
+        The threshold (in bytes) of free disk space left at which to
+        start garbage collection on the runner
+      '';
+    };
+    max-free = mkOption {
+      default = 3 * 1024 * 1024 * 1024;
+      type = types.int;
+      example = 3221225472;
+      description = ''
+        The threshold (in bytes) of free disk space left at which to
+        stop garbage collection on the runner
+      '';
+    };
+    workingDirectory = mkOption {
+       default = ".";
+       type = types.str;
+       example = "/var/lib/darwin-builder";
+       description = ''
+         The working directory to use to run the script. When running
+         as part of a flake will need to be set to a non read-only filesystem.
+       '';
+    };
+    hostPort = mkOption {
+      default = 31022;
+      type = types.int;
+      example = 22;
+      description = ''
+        The localhost host port to forward TCP to the guest port.
+      '';
     };
   };
 
-  # DNS fails for QEMU user networking (SLiRP) on macOS.  See:
-  #
-  # https://github.com/utmapp/UTM/issues/2353
-  #
-  # This works around that by using a public DNS server other than the DNS
-  # server that QEMU provides (normally 10.0.2.3)
-  networking.nameservers = [ "8.8.8.8" ];
+  config = {
+    # The builder is not intended to be used interactively
+    documentation.enable = false;
 
-  nix.settings = {
-    auto-optimise-store = true;
+    environment.etc = {
+      "ssh/ssh_host_ed25519_key" = {
+        mode = "0600";
 
-    min-free = 1024 * 1024 * 1024;
+        source = ./keys/ssh_host_ed25519_key;
+      };
 
-    max-free = 3 * 1024 * 1024 * 1024;
+      "ssh/ssh_host_ed25519_key.pub" = {
+        mode = "0644";
 
-    trusted-users = [ "root" user ];
-  };
+        source = ./keys/ssh_host_ed25519_key.pub;
+      };
+    };
+
+    # DNS fails for QEMU user networking (SLiRP) on macOS.  See:
+    #
+    # https://github.com/utmapp/UTM/issues/2353
+    #
+    # This works around that by using a public DNS server other than the DNS
+    # server that QEMU provides (normally 10.0.2.3)
+    networking.nameservers = [ "8.8.8.8" ];
 
-  services.openssh = {
-    enable = true;
+    nix.settings = {
+      auto-optimise-store = true;
 
-    authorizedKeysFiles = [ "${keysDirectory}/%u_${keyType}.pub" ];
-  };
+      min-free = cfg.min-free;
 
-  system.build.macos-builder-installer =
-    let
-      privateKey = "/etc/nix/${user}_${keyType}";
+      max-free = cfg.max-free;
 
-      publicKey = "${privateKey}.pub";
+      trusted-users = [ "root" user ];
+    };
 
-      # This installCredentials script is written so that it's as easy as
-      # possible for a user to audit before confirming the `sudo`
-      installCredentials = hostPkgs.writeShellScript "install-credentials" ''
-        KEYS="''${1}"
-        INSTALL=${hostPkgs.coreutils}/bin/install
-        "''${INSTALL}" -g nixbld -m 600 "''${KEYS}/${user}_${keyType}" ${privateKey}
-        "''${INSTALL}" -g nixbld -m 644 "''${KEYS}/${user}_${keyType}.pub" ${publicKey}
-      '';
+    services = {
+      getty.autologinUser = user;
 
-      hostPkgs = config.virtualisation.host.pkgs;
-
-      script = hostPkgs.writeShellScriptBin "create-builder" ''
-        KEYS="''${KEYS:-./keys}"
-        ${hostPkgs.coreutils}/bin/mkdir --parent "''${KEYS}"
-        PRIVATE_KEY="''${KEYS}/${user}_${keyType}"
-        PUBLIC_KEY="''${PRIVATE_KEY}.pub"
-        if [ ! -e "''${PRIVATE_KEY}" ] || [ ! -e "''${PUBLIC_KEY}" ]; then
-            ${hostPkgs.coreutils}/bin/rm --force -- "''${PRIVATE_KEY}" "''${PUBLIC_KEY}"
-            ${hostPkgs.openssh}/bin/ssh-keygen -q -f "''${PRIVATE_KEY}" -t ${keyType} -N "" -C 'builder@localhost'
-        fi
-        if ! ${hostPkgs.diffutils}/bin/cmp "''${PUBLIC_KEY}" ${publicKey}; then
-          (set -x; sudo --reset-timestamp ${installCredentials} "''${KEYS}")
-        fi
-        KEYS="$(nix-store --add "$KEYS")" ${config.system.build.vm}/bin/run-nixos-vm
-      '';
+      openssh = {
+        enable = true;
 
-    in
-    script.overrideAttrs (old: {
-      meta = (old.meta or { }) // {
-        platforms = lib.platforms.darwin;
+        authorizedKeysFiles = [ "${keysDirectory}/%u_${keyType}.pub" ];
       };
-    });
-
-  system = {
-    # To prevent gratuitous rebuilds on each change to Nixpkgs
-    nixos.revision = null;
+    };
 
-    stateVersion = lib.mkDefault (throw ''
-      The macOS linux builder should not need a stateVersion to be set, but a module
-      has accessed stateVersion nonetheless.
-      Please inspect the trace of the following command to figure out which module
-      has a dependency on stateVersion.
+    system.build.macos-builder-installer =
+      let
+        privateKey = "/etc/nix/${user}_${keyType}";
+
+        publicKey = "${privateKey}.pub";
+
+        # This installCredentials script is written so that it's as easy as
+        # possible for a user to audit before confirming the `sudo`
+        installCredentials = hostPkgs.writeShellScript "install-credentials" ''
+          KEYS="''${1}"
+          INSTALL=${hostPkgs.coreutils}/bin/install
+          "''${INSTALL}" -g nixbld -m 600 "''${KEYS}/${user}_${keyType}" ${privateKey}
+          "''${INSTALL}" -g nixbld -m 644 "''${KEYS}/${user}_${keyType}.pub" ${publicKey}
+        '';
+
+        hostPkgs = config.virtualisation.host.pkgs;
+
+        script = hostPkgs.writeShellScriptBin "create-builder" (
+          # When running as non-interactively as part of a DarwinConfiguration the working directory
+          # must be set to a writeable directory.
+        (if cfg.workingDirectory != "." then ''
+          ${hostPkgs.coreutils}/bin/mkdir --parent "${cfg.workingDirectory}"
+          cd "${cfg.workingDirectory}"
+        '' else "") + ''
+          KEYS="''${KEYS:-./keys}"
+          ${hostPkgs.coreutils}/bin/mkdir --parent "''${KEYS}"
+          PRIVATE_KEY="''${KEYS}/${user}_${keyType}"
+          PUBLIC_KEY="''${PRIVATE_KEY}.pub"
+          if [ ! -e "''${PRIVATE_KEY}" ] || [ ! -e "''${PUBLIC_KEY}" ]; then
+              ${hostPkgs.coreutils}/bin/rm --force -- "''${PRIVATE_KEY}" "''${PUBLIC_KEY}"
+              ${hostPkgs.openssh}/bin/ssh-keygen -q -f "''${PRIVATE_KEY}" -t ${keyType} -N "" -C 'builder@localhost'
+          fi
+          if ! ${hostPkgs.diffutils}/bin/cmp "''${PUBLIC_KEY}" ${publicKey}; then
+            (set -x; sudo --reset-timestamp ${installCredentials} "''${KEYS}")
+          fi
+          KEYS="$(${hostPkgs.nix}/bin/nix-store --add "$KEYS")" ${lib.getExe config.system.build.vm}
+        '');
+
+      in
+      script.overrideAttrs (old: {
+        meta = (old.meta or { }) // {
+          platforms = lib.platforms.darwin;
+        };
+      });
+
+    system = {
+      # To prevent gratuitous rebuilds on each change to Nixpkgs
+      nixos.revision = null;
+
+      stateVersion = lib.mkDefault (throw ''
+        The macOS linux builder should not need a stateVersion to be set, but a module
+        has accessed stateVersion nonetheless.
+        Please inspect the trace of the following command to figure out which module
+        has a dependency on stateVersion.
+
+          nix-instantiate --attr darwin.linux-builder --show-trace
+      '');
+    };
 
-        nix-instantiate --attr darwin.builder --show-trace
-    '');
-  };
+    users.users."${user}" = {
+      isNormalUser = true;
+    };
 
-  users.users."${user}" = {
-    isNormalUser = true;
-  };
+    security.polkit.enable = true;
 
-  security.polkit.enable = true;
+    security.polkit.extraConfig = ''
+      polkit.addRule(function(action, subject) {
+        if (action.id === "org.freedesktop.login1.power-off" && subject.user === "${user}") {
+          return "yes";
+        } else {
+          return "no";
+        }
+      })
+    '';
 
-  security.polkit.extraConfig = ''
-    polkit.addRule(function(action, subject) {
-      if (action.id === "org.freedesktop.login1.power-off" && subject.user === "${user}") {
-        return "yes";
-      } else {
-        return "no";
-      }
-    })
-  '';
+    virtualisation = {
+      diskSize = cfg.diskSize;
 
-  virtualisation = {
-    diskSize = 20 * 1024;
+      memorySize = cfg.memorySize;
 
-    memorySize = 3 * 1024;
+      forwardPorts = [
+        { from = "host"; guest.port = 22; host.port = cfg.hostPort; }
+      ];
 
-    forwardPorts = [
-      { from = "host"; guest.port = 22; host.port = 22; }
-    ];
+      # Disable graphics for the builder since users will likely want to run it
+      # non-interactively in the background.
+      graphics = false;
 
-    # Disable graphics for the builder since users will likely want to run it
-    # non-interactively in the background.
-    graphics = false;
+      sharedDirectories.keys = {
+        source = "\"$KEYS\"";
+        target = keysDirectory;
+      };
 
-    sharedDirectories.keys = {
-      source = "\"$KEYS\"";
-      target = keysDirectory;
+      # If we don't enable this option then the host will fail to delegate builds
+      # to the guest, because:
+      #
+      # - The host will lock the path to build
+      # - The host will delegate the build to the guest
+      # - The guest will attempt to lock the same path and fail because
+      #   the lockfile on the host is visible on the guest
+      #
+      # Snapshotting the host's /nix/store as an image isolates the guest VM's
+      # /nix/store from the host's /nix/store, preventing this problem.
+      useNixStoreImage = true;
+
+      # Obviously the /nix/store needs to be writable on the guest in order for it
+      # to perform builds.
+      writableStore = true;
+
+      # This ensures that anything built on the guest isn't lost when the guest is
+      # restarted.
+      writableStoreUseTmpfs = false;
+
+      # Pass certificates from host to the guest otherwise when custom CA certificates
+      # are required we can't use the cached builder.
+      useHostCerts = true;
     };
-
-    # If we don't enable this option then the host will fail to delegate builds
-    # to the guest, because:
-    #
-    # - The host will lock the path to build
-    # - The host will delegate the build to the guest
-    # - The guest will attempt to lock the same path and fail because
-    #   the lockfile on the host is visible on the guest
-    #
-    # Snapshotting the host's /nix/store as an image isolates the guest VM's
-    # /nix/store from the host's /nix/store, preventing this problem.
-    useNixStoreImage = true;
-
-    # Obviously the /nix/store needs to be writable on the guest in order for it
-    # to perform builds.
-    writableStore = true;
-
-    # This ensures that anything built on the guest isn't lost when the guest is
-    # restarted.
-    writableStoreUseTmpfs = false;
   };
 }
diff --git a/nixos/modules/programs/_1password-gui.nix b/nixos/modules/programs/_1password-gui.nix
index 83ef6037fb5..27c0d34a2ee 100644
--- a/nixos/modules/programs/_1password-gui.nix
+++ b/nixos/modules/programs/_1password-gui.nix
@@ -27,7 +27,7 @@ in
         '';
       };
 
-      package = mkPackageOption pkgs "1Password GUI" {
+      package = mkPackageOptionMD pkgs "1Password GUI" {
         default = [ "_1password-gui" ];
       };
     };
diff --git a/nixos/modules/programs/_1password.nix b/nixos/modules/programs/_1password.nix
index 91246150755..8537484c7e6 100644
--- a/nixos/modules/programs/_1password.nix
+++ b/nixos/modules/programs/_1password.nix
@@ -18,7 +18,7 @@ in
     programs._1password = {
       enable = mkEnableOption (lib.mdDoc "the 1Password CLI tool");
 
-      package = mkPackageOption pkgs "1Password CLI" {
+      package = mkPackageOptionMD pkgs "1Password CLI" {
         default = [ "_1password" ];
       };
     };
diff --git a/nixos/modules/programs/atop.nix b/nixos/modules/programs/atop.nix
index 2b14d7c7343..9d5843bd670 100644
--- a/nixos/modules/programs/atop.nix
+++ b/nixos/modules/programs/atop.nix
@@ -142,6 +142,7 @@ in
               # convert remainings logs and start eventually
               atop.serviceConfig.ExecStartPre = pkgs.writeShellScript "atop-update-log-format" ''
                 set -e -u
+                shopt -s nullglob
                 for logfile in "$LOGPATH"/atop_*
                 do
                   ${atop}/bin/atopconvert "$logfile" "$logfile".new
@@ -150,6 +151,8 @@ in
                   if ! ${pkgs.diffutils}/bin/cmp -s "$logfile" "$logfile".new
                   then
                     ${pkgs.coreutils}/bin/mv -v -f "$logfile".new "$logfile"
+                  else
+                    ${pkgs.coreutils}/bin/rm -f "$logfile".new
                   fi
                 done
               '';
diff --git a/nixos/modules/programs/ccache.nix b/nixos/modules/programs/ccache.nix
index 19fb7ca3294..567c853e8c7 100644
--- a/nixos/modules/programs/ccache.nix
+++ b/nixos/modules/programs/ccache.nix
@@ -17,7 +17,7 @@ in {
       type = types.listOf types.str;
       description = lib.mdDoc "Nix top-level packages to be compiled using CCache";
       default = [];
-      example = [ "wxGTK30" "ffmpeg" "libav_all" ];
+      example = [ "wxGTK32" "ffmpeg" "libav_all" ];
     };
   };
 
diff --git a/nixos/modules/programs/cfs-zen-tweaks.nix b/nixos/modules/programs/cfs-zen-tweaks.nix
index 97c2570475c..fc05bcd11ec 100644
--- a/nixos/modules/programs/cfs-zen-tweaks.nix
+++ b/nixos/modules/programs/cfs-zen-tweaks.nix
@@ -23,6 +23,12 @@ in
   config = mkIf cfg.enable {
     systemd.packages = [ pkgs.cfs-zen-tweaks ];
 
-    systemd.services.set-cfs-tweak.wantedBy = [ "multi-user.target" "suspend.target" "hibernate.target" "hybrid-sleep.target" "suspend-then-hibernate.target" ];
+    systemd.services.set-cfs-tweaks.wantedBy = [
+      "multi-user.target"
+      "suspend.target"
+      "hibernate.target"
+      "hybrid-sleep.target"
+      "suspend-then-hibernate.target"
+    ];
   };
 }
diff --git a/nixos/modules/programs/clash-verge.nix b/nixos/modules/programs/clash-verge.nix
new file mode 100644
index 00000000000..29977be3858
--- /dev/null
+++ b/nixos/modules/programs/clash-verge.nix
@@ -0,0 +1,41 @@
+{ config, lib, pkgs, ... }:
+
+{
+  options.programs.clash-verge = {
+    enable = lib.mkEnableOption (lib.mdDoc ''
+      Clash Verge.
+    '');
+
+    autoStart = lib.mkEnableOption (lib.mdDoc ''
+      Clash Verge Auto Launch.
+    '');
+
+    tunMode = lib.mkEnableOption (lib.mdDoc ''
+      Clash Verge Tun Mode.
+    '');
+  };
+
+  config =
+    let
+      cfg = config.programs.clash-verge;
+    in
+    lib.mkIf cfg.enable {
+
+      environment.systemPackages = [
+        pkgs.clash-verge
+        (lib.mkIf cfg.autoStart (pkgs.makeAutostartItem {
+          name = "clash-verge";
+          package = pkgs.clash-verge;
+        }))
+      ];
+
+      security.wrappers.clash-verge = lib.mkIf cfg.tunMode {
+        owner = "root";
+        group = "root";
+        capabilities = "cap_net_bind_service,cap_net_admin=+ep";
+        source = "${lib.getExe pkgs.clash-verge}";
+      };
+    };
+
+  meta.maintainers = with lib.maintainers; [ zendo ];
+}
diff --git a/nixos/modules/programs/darling.nix b/nixos/modules/programs/darling.nix
new file mode 100644
index 00000000000..c4e1c73b5c2
--- /dev/null
+++ b/nixos/modules/programs/darling.nix
@@ -0,0 +1,21 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.darling;
+in {
+  options = {
+    programs.darling = {
+      enable = lib.mkEnableOption (lib.mdDoc "Darling, a Darwin/macOS compatibility layer for Linux");
+      package = lib.mkPackageOptionMD pkgs "darling" {};
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    security.wrappers.darling = {
+      source = lib.getExe cfg.package;
+      owner = "root";
+      group = "root";
+      setuid = true;
+    };
+  };
+}
diff --git a/nixos/modules/programs/digitalbitbox/default.md b/nixos/modules/programs/digitalbitbox/default.md
new file mode 100644
index 00000000000..9bca14e97ff
--- /dev/null
+++ b/nixos/modules/programs/digitalbitbox/default.md
@@ -0,0 +1,47 @@
+# Digital Bitbox {#module-programs-digitalbitbox}
+
+Digital Bitbox is a hardware wallet and second-factor authenticator.
+
+The `digitalbitbox` programs module may be installed by setting
+`programs.digitalbitbox` to `true` in a manner similar to
+```
+programs.digitalbitbox.enable = true;
+```
+and bundles the `digitalbitbox` package (see [](#sec-digitalbitbox-package)),
+which contains the `dbb-app` and `dbb-cli` binaries, along with the hardware
+module (see [](#sec-digitalbitbox-hardware-module)) which sets up the necessary
+udev rules to access the device.
+
+Enabling the digitalbitbox module is pretty much the easiest way to get a
+Digital Bitbox device working on your system.
+
+For more information, see <https://digitalbitbox.com/start_linux>.
+
+## Package {#sec-digitalbitbox-package}
+
+The binaries, `dbb-app` (a GUI tool) and `dbb-cli` (a CLI tool), are available
+through the `digitalbitbox` package which could be installed as follows:
+```
+environment.systemPackages = [
+  pkgs.digitalbitbox
+];
+```
+
+## Hardware {#sec-digitalbitbox-hardware-module}
+
+The digitalbitbox hardware package enables the udev rules for Digital Bitbox
+devices and may be installed as follows:
+```
+hardware.digitalbitbox.enable = true;
+```
+
+In order to alter the udev rules, one may provide different values for the
+`udevRule51` and `udevRule52` attributes by means of overriding as follows:
+```
+programs.digitalbitbox = {
+  enable = true;
+  package = pkgs.digitalbitbox.override {
+    udevRule51 = "something else";
+  };
+};
+```
diff --git a/nixos/modules/programs/digitalbitbox/default.nix b/nixos/modules/programs/digitalbitbox/default.nix
index 101ee8ddbaf..5ee6cdafe63 100644
--- a/nixos/modules/programs/digitalbitbox/default.nix
+++ b/nixos/modules/programs/digitalbitbox/default.nix
@@ -33,7 +33,7 @@ in
   };
 
   meta = {
-    doc = ./doc.xml;
+    doc = ./default.md;
     maintainers = with lib.maintainers; [ vidbina ];
   };
 }
diff --git a/nixos/modules/programs/digitalbitbox/doc.xml b/nixos/modules/programs/digitalbitbox/doc.xml
deleted file mode 100644
index c63201628db..00000000000
--- a/nixos/modules/programs/digitalbitbox/doc.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-programs-digitalbitbox">
- <title>Digital Bitbox</title>
- <para>
-  Digital Bitbox is a hardware wallet and second-factor authenticator.
- </para>
- <para>
-  The <literal>digitalbitbox</literal> programs module may be installed by
-  setting <literal>programs.digitalbitbox</literal> to <literal>true</literal>
-  in a manner similar to
-<programlisting>
-<xref linkend="opt-programs.digitalbitbox.enable"/> = true;
-</programlisting>
-  and bundles the <literal>digitalbitbox</literal> package (see
-  <xref
-      linkend="sec-digitalbitbox-package" />), which contains the
-  <literal>dbb-app</literal> and <literal>dbb-cli</literal> binaries, along
-  with the hardware module (see
-  <xref
-      linkend="sec-digitalbitbox-hardware-module" />) which sets up the
-  necessary udev rules to access the device.
- </para>
- <para>
-  Enabling the digitalbitbox module is pretty much the easiest way to get a
-  Digital Bitbox device working on your system.
- </para>
- <para>
-  For more information, see
-  <link xlink:href="https://digitalbitbox.com/start_linux" />.
- </para>
- <section xml:id="sec-digitalbitbox-package">
-  <title>Package</title>
-
-  <para>
-   The binaries, <literal>dbb-app</literal> (a GUI tool) and
-   <literal>dbb-cli</literal> (a CLI tool), are available through the
-   <literal>digitalbitbox</literal> package which could be installed as
-   follows:
-<programlisting>
-<xref linkend="opt-environment.systemPackages"/> = [
-  pkgs.digitalbitbox
-];
-</programlisting>
-  </para>
- </section>
- <section xml:id="sec-digitalbitbox-hardware-module">
-  <title>Hardware</title>
-
-  <para>
-   The digitalbitbox hardware package enables the udev rules for Digital Bitbox
-   devices and may be installed as follows:
-<programlisting>
-<xref linkend="opt-hardware.digitalbitbox.enable"/> = true;
-</programlisting>
-  </para>
-
-  <para>
-   In order to alter the udev rules, one may provide different values for the
-   <literal>udevRule51</literal> and <literal>udevRule52</literal> attributes
-   by means of overriding as follows:
-<programlisting>
-programs.digitalbitbox = {
-  <link linkend="opt-programs.digitalbitbox.enable">enable</link> = true;
-  <link linkend="opt-programs.digitalbitbox.package">package</link> = pkgs.digitalbitbox.override {
-    udevRule51 = "something else";
-  };
-};
-</programlisting>
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/programs/firefox.nix b/nixos/modules/programs/firefox.nix
index 3a5105c57d7..ead048134d8 100644
--- a/nixos/modules/programs/firefox.nix
+++ b/nixos/modules/programs/firefox.nix
@@ -201,6 +201,7 @@ in
     nativeMessagingHosts = mapAttrs (_: v: mkEnableOption (mdDoc v)) {
       browserpass = "Browserpass support";
       bukubrow = "Bukubrow support";
+      euwebid = "Web eID support";
       ff2mpv = "ff2mpv support";
       fxCast = "fx_cast support";
       gsconnect = "GSConnect support";
@@ -217,6 +218,8 @@ in
         extraPrefs = cfg.autoConfig;
         extraNativeMessagingHosts = with pkgs; optionals nmh.ff2mpv [
           ff2mpv
+        ] ++ optionals nmh.euwebid [
+          web-eid-app
         ] ++ optionals nmh.gsconnect [
           gnomeExtensions.gsconnect
         ] ++ optionals nmh.jabref [
@@ -230,6 +233,7 @@ in
     nixpkgs.config.firefox = {
       enableBrowserpass = nmh.browserpass;
       enableBukubrow = nmh.bukubrow;
+      enableEUWebID = nmh.euwebid;
       enableTridactylNative = nmh.tridactyl;
       enableUgetIntegrator = nmh.ugetIntegrator;
       enableFXCastBridge = nmh.fxCast;
diff --git a/nixos/modules/programs/fish.nix b/nixos/modules/programs/fish.nix
index 160adc0cad6..478f07d0131 100644
--- a/nixos/modules/programs/fish.nix
+++ b/nixos/modules/programs/fish.nix
@@ -303,7 +303,7 @@ in
     programs.fish.interactiveShellInit = ''
       # add completions generated by NixOS to $fish_complete_path
       begin
-        # joins with null byte to acommodate all characters in paths, then respectively gets all paths before (exclusive) / after (inclusive) the first one including "generated_completions",
+        # joins with null byte to accommodate all characters in paths, then respectively gets all paths before (exclusive) / after (inclusive) the first one including "generated_completions",
         # splits by null byte, and then removes all empty lines produced by using 'string'
         set -l prev (string join0 $fish_complete_path | string match --regex "^.*?(?=\x00[^\x00]*generated_completions.*)" | string split0 | string match -er ".")
         set -l post (string join0 $fish_complete_path | string match --regex "[^\x00]*generated_completions.*" | string split0 | string match -er ".")
diff --git a/nixos/modules/programs/flashrom.nix b/nixos/modules/programs/flashrom.nix
index ff495558c9e..9f8faff14e4 100644
--- a/nixos/modules/programs/flashrom.nix
+++ b/nixos/modules/programs/flashrom.nix
@@ -16,12 +16,11 @@ in
         group.
       '';
     };
-    package = mkPackageOption pkgs "flashrom" { };
+    package = mkPackageOptionMD pkgs "flashrom" { };
   };
 
   config = mkIf cfg.enable {
     services.udev.packages = [ cfg.package ];
     environment.systemPackages = [ cfg.package ];
-    users.groups.flashrom = { };
   };
 }
diff --git a/nixos/modules/programs/fzf.nix b/nixos/modules/programs/fzf.nix
index eda4eacde4a..7c4f338e29b 100644
--- a/nixos/modules/programs/fzf.nix
+++ b/nixos/modules/programs/fzf.nix
@@ -1,8 +1,9 @@
-{pkgs, config, lib, ...}:
+{ pkgs, config, lib, ... }:
 with lib;
 let
   cfg = config.programs.fzf;
-in {
+in
+{
   options = {
     programs.fzf = {
       fuzzyCompletion = mkEnableOption (mdDoc "fuzzy completion with fzf");
@@ -11,17 +12,21 @@ in {
   };
   config = {
     environment.systemPackages = optional (cfg.keybindings || cfg.fuzzyCompletion) pkgs.fzf;
+
     programs.bash.interactiveShellInit = optionalString cfg.fuzzyCompletion ''
       source ${pkgs.fzf}/share/fzf/completion.bash
     '' + optionalString cfg.keybindings ''
       source ${pkgs.fzf}/share/fzf/key-bindings.bash
     '';
 
-    programs.zsh.interactiveShellInit = optionalString cfg.fuzzyCompletion ''
-      source ${pkgs.fzf}/share/fzf/completion.zsh
-    '' + optionalString cfg.keybindings ''
-      source ${pkgs.fzf}/share/fzf/key-bindings.zsh
-    '';
+    programs.zsh.interactiveShellInit = optionalString (!config.programs.zsh.ohMyZsh.enable)
+      (optionalString cfg.fuzzyCompletion ''
+        source ${pkgs.fzf}/share/fzf/completion.zsh
+      '' + optionalString cfg.keybindings ''
+        source ${pkgs.fzf}/share/fzf/key-bindings.zsh
+      '');
+
+    programs.zsh.ohMyZsh.plugins = lib.mkIf (cfg.keybindings || cfg.fuzzyCompletion) [ "fzf" ];
   };
   meta.maintainers = with maintainers; [ laalsaas ];
 }
diff --git a/nixos/modules/programs/gamescope.nix b/nixos/modules/programs/gamescope.nix
new file mode 100644
index 00000000000..c4424849a41
--- /dev/null
+++ b/nixos/modules/programs/gamescope.nix
@@ -0,0 +1,85 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+with lib; let
+  cfg = config.programs.gamescope;
+
+  gamescope =
+    let
+      wrapperArgs =
+        optional (cfg.args != [ ])
+          ''--add-flags "${toString cfg.args}"''
+        ++ builtins.attrValues (mapAttrs (var: val: "--set-default ${var} ${val}") cfg.env);
+    in
+    pkgs.runCommand "gamescope" { nativeBuildInputs = [ pkgs.makeBinaryWrapper ]; } ''
+      mkdir -p $out/bin
+      makeWrapper ${cfg.package}/bin/gamescope $out/bin/gamescope --inherit-argv0 \
+        ${toString wrapperArgs}
+    '';
+in
+{
+  options.programs.gamescope = {
+    enable = mkEnableOption (mdDoc "gamescope");
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.gamescope;
+      defaultText = literalExpression "pkgs.gamescope";
+      description = mdDoc ''
+        The GameScope package to use.
+      '';
+    };
+
+    capSysNice = mkOption {
+      type = types.bool;
+      default = false;
+      description = mdDoc ''
+        Add cap_sys_nice capability to the GameScope
+        binary so that it may renice itself.
+      '';
+    };
+
+    args = mkOption {
+      type = types.listOf types.string;
+      default = [ ];
+      example = [ "--rt" "--prefer-vk-device 8086:9bc4" ];
+      description = mdDoc ''
+        Arguments passed to GameScope on startup.
+      '';
+    };
+
+    env = mkOption {
+      type = types.attrsOf types.string;
+      default = { };
+      example = literalExpression ''
+        # for Prime render offload on Nvidia laptops.
+        # Also requires `hardware.nvidia.prime.offload.enable`.
+        {
+          __NV_PRIME_RENDER_OFFLOAD = "1";
+          __VK_LAYER_NV_optimus = "NVIDIA_only";
+          __GLX_VENDOR_LIBRARY_NAME = "nvidia";
+        }
+      '';
+      description = mdDoc ''
+        Default environment variables available to the GameScope process, overridable at runtime.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    security.wrappers = mkIf cfg.capSysNice {
+      gamescope = {
+        owner = "root";
+        group = "root";
+        source = "${gamescope}/bin/gamescope";
+        capabilities = "cap_sys_nice+pie";
+      };
+    };
+
+    environment.systemPackages = mkIf (!cfg.capSysNice) [ gamescope ];
+  };
+
+  meta.maintainers = with maintainers; [ nrdxp ];
+}
diff --git a/nixos/modules/programs/gnome-documents.nix b/nixos/modules/programs/gnome-documents.nix
deleted file mode 100644
index 2831ac9aff2..00000000000
--- a/nixos/modules/programs/gnome-documents.nix
+++ /dev/null
@@ -1,54 +0,0 @@
-# GNOME Documents.
-
-{ config, pkgs, lib, ... }:
-
-with lib;
-
-{
-
-  meta = {
-    maintainers = teams.gnome.members;
-  };
-
-  # Added 2019-08-09
-  imports = [
-    (mkRenamedOptionModule
-      [ "services" "gnome" "gnome-documents" "enable" ]
-      [ "programs" "gnome-documents" "enable" ])
-  ];
-
-  ###### interface
-
-  options = {
-
-    programs.gnome-documents = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to enable GNOME Documents, a document
-          manager application for GNOME.
-        '';
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf config.programs.gnome-documents.enable {
-
-    environment.systemPackages = [ pkgs.gnome.gnome-documents ];
-
-    services.dbus.packages = [ pkgs.gnome.gnome-documents ];
-
-    services.gnome.gnome-online-accounts.enable = true;
-
-    services.gnome.gnome-online-miners.enable = true;
-
-  };
-
-}
diff --git a/nixos/modules/programs/gnupg.nix b/nixos/modules/programs/gnupg.nix
index cb8d0ecff4c..697b6e9a0bd 100644
--- a/nixos/modules/programs/gnupg.nix
+++ b/nixos/modules/programs/gnupg.nix
@@ -10,7 +10,8 @@ let
 
   defaultPinentryFlavor =
     if xserverCfg.desktopManager.lxqt.enable
-    || xserverCfg.desktopManager.plasma5.enable then
+    || xserverCfg.desktopManager.plasma5.enable
+    || xserverCfg.desktopManager.deepin.enable then
       "qt"
     else if xserverCfg.desktopManager.xfce.enable then
       "gtk2"
@@ -74,9 +75,7 @@ in
       defaultText = literalMD ''matching the configured desktop environment'';
       description = lib.mdDoc ''
         Which pinentry interface to use. If not null, the path to the
-        pinentry binary will be passed to gpg-agent via commandline and
-        thus overrides the pinentry option in gpg-agent.conf in the user's
-        home directory.
+        pinentry binary will be set in /etc/gnupg/gpg-agent.conf.
         If not set at all, it'll pick an appropriate flavor depending on the
         system configuration (qt flavor for lxqt and plasma5, gtk2 for xfce
         4.12, gnome3 on all other systems with X enabled, ncurses otherwise).
@@ -93,38 +92,111 @@ in
   };
 
   config = mkIf cfg.agent.enable {
+    environment.etc."gnupg/gpg-agent.conf".text =
+      lib.optionalString (cfg.agent.pinentryFlavor != null) ''
+      pinentry-program ${pkgs.pinentry.${cfg.agent.pinentryFlavor}}/bin/pinentry
+    '';
+
     # This overrides the systemd user unit shipped with the gnupg package
-    systemd.user.services.gpg-agent = mkIf (cfg.agent.pinentryFlavor != null) {
-      serviceConfig.ExecStart = [ "" ''
-        ${cfg.package}/bin/gpg-agent --supervised \
-          --pinentry-program ${pkgs.pinentry.${cfg.agent.pinentryFlavor}}/bin/pinentry
-      '' ];
+    systemd.user.services.gpg-agent = {
+      unitConfig = {
+        Description = "GnuPG cryptographic agent and passphrase cache";
+        Documentation = "man:gpg-agent(1)";
+        Requires = [ "gpg-agent.socket" ];
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/gpg-agent --supervised";
+        ExecReload = "${cfg.package}/bin/gpgconf --reload gpg-agent";
+      };
     };
 
     systemd.user.sockets.gpg-agent = {
+      unitConfig = {
+        Description = "GnuPG cryptographic agent and passphrase cache";
+        Documentation = "man:gpg-agent(1)";
+      };
+      socketConfig = {
+        ListenStream = "%t/gnupg/S.gpg-agent";
+        FileDescriptorName = "std";
+        SocketMode = "0600";
+        DirectoryMode = "0700";
+      };
       wantedBy = [ "sockets.target" ];
     };
 
     systemd.user.sockets.gpg-agent-ssh = mkIf cfg.agent.enableSSHSupport {
+      unitConfig = {
+        Description = "GnuPG cryptographic agent (ssh-agent emulation)";
+        Documentation = "man:gpg-agent(1) man:ssh-add(1) man:ssh-agent(1) man:ssh(1)";
+      };
+      socketConfig = {
+        ListenStream = "%t/gnupg/S.gpg-agent.ssh";
+        FileDescriptorName = "ssh";
+        Service = "gpg-agent.service";
+        SocketMode = "0600";
+        DirectoryMode = "0700";
+      };
       wantedBy = [ "sockets.target" ];
     };
 
     systemd.user.sockets.gpg-agent-extra = mkIf cfg.agent.enableExtraSocket {
+      unitConfig = {
+        Description = "GnuPG cryptographic agent and passphrase cache (restricted)";
+        Documentation = "man:gpg-agent(1)";
+      };
+      socketConfig = {
+        ListenStream = "%t/gnupg/S.gpg-agent.extra";
+        FileDescriptorName = "extra";
+        Service = "gpg-agent.service";
+        SocketMode = "0600";
+        DirectoryMode = "0700";
+      };
       wantedBy = [ "sockets.target" ];
     };
 
     systemd.user.sockets.gpg-agent-browser = mkIf cfg.agent.enableBrowserSocket {
+      unitConfig = {
+        Description = "GnuPG cryptographic agent and passphrase cache (access for web browsers)";
+        Documentation = "man:gpg-agent(1)";
+      };
+      socketConfig = {
+        ListenStream = "%t/gnupg/S.gpg-agent.browser";
+        FileDescriptorName = "browser";
+        Service = "gpg-agent.service";
+        SocketMode = "0600";
+        DirectoryMode = "0700";
+      };
       wantedBy = [ "sockets.target" ];
     };
 
+    systemd.user.services.dirmngr = mkIf cfg.dirmngr.enable {
+      unitConfig = {
+        Description = "GnuPG network certificate management daemon";
+        Documentation = "man:dirmngr(8)";
+        Requires = "dirmngr.socket";
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/dirmngr --supervised";
+        ExecReload = "${cfg.package}/bin/gpgconf --reload dirmngr";
+      };
+    };
+
     systemd.user.sockets.dirmngr = mkIf cfg.dirmngr.enable {
+      unitConfig = {
+        Description = "GnuPG network certificate management daemon";
+        Documentation = "man:dirmngr(8)";
+      };
+      socketConfig = {
+        ListenStream = "%t/gnupg/S.dirmngr";
+        SocketMode = "0600";
+        DirectoryMode = "0700";
+      };
       wantedBy = [ "sockets.target" ];
     };
 
     services.dbus.packages = mkIf (cfg.agent.pinentryFlavor == "gnome3") [ pkgs.gcr ];
 
     environment.systemPackages = with pkgs; [ cfg.package ];
-    systemd.packages = [ cfg.package ];
 
     environment.interactiveShellInit = ''
       # Bind gpg-agent to this TTY if gpg commands are used.
diff --git a/nixos/modules/programs/hyprland.nix b/nixos/modules/programs/hyprland.nix
new file mode 100644
index 00000000000..92b8e992e64
--- /dev/null
+++ b/nixos/modules/programs/hyprland.nix
@@ -0,0 +1,81 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+with lib; let
+  cfg = config.programs.hyprland;
+
+  defaultHyprlandPackage = pkgs.hyprland.override {
+    enableXWayland = cfg.xwayland.enable;
+    hidpiXWayland = cfg.xwayland.hidpi;
+    nvidiaPatches = cfg.nvidiaPatches;
+  };
+in
+{
+  options.programs.hyprland = {
+    enable = mkEnableOption null // {
+      description = mdDoc ''
+        Hyprland, the dynamic tiling Wayland compositor that doesn't sacrifice on its looks.
+
+        You can manually launch Hyprland by executing {command}`Hyprland` on a TTY.
+
+        A configuration file will be generated in {file}`~/.config/hypr/hyprland.conf`.
+        See <https://wiki.hyprland.org> for more information.
+      '';
+    };
+
+    package = mkOption {
+      type = types.path;
+      default = defaultHyprlandPackage;
+      defaultText = literalExpression ''
+        pkgs.hyprland.override {
+          enableXWayland = config.programs.hyprland.xwayland.enable;
+          hidpiXWayland = config.programs.hyprland.xwayland.hidpi;
+          nvidiaPatches = config.programs.hyprland.nvidiaPatches;
+        }
+      '';
+      example = literalExpression "<Hyprland flake>.packages.<system>.default";
+      description = mdDoc ''
+        The Hyprland package to use.
+        Setting this option will make {option}`programs.hyprland.xwayland` and
+        {option}`programs.hyprland.nvidiaPatches` not work.
+      '';
+    };
+
+    xwayland = {
+      enable = mkEnableOption (mdDoc "XWayland") // { default = true; };
+      hidpi = mkEnableOption null // {
+        description = mdDoc ''
+          Enable HiDPI XWayland, based on [XWayland MR 733](https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/733).
+          See <https://wiki.hyprland.org/Nix/Options-Overrides/#xwayland-hidpi> for more info.
+        '';
+      };
+    };
+
+    nvidiaPatches = mkEnableOption (mdDoc "patching wlroots for better Nvidia support");
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    fonts.enableDefaultFonts = mkDefault true;
+    hardware.opengl.enable = mkDefault true;
+
+    programs = {
+      dconf.enable = mkDefault true;
+      xwayland.enable = mkDefault cfg.xwayland.enable;
+    };
+
+    security.polkit.enable = true;
+
+    services.xserver.displayManager.sessionPackages = [ cfg.package ];
+
+    xdg.portal = {
+      enable = mkDefault true;
+      extraPortals = [
+        pkgs.xdg-desktop-portal-hyprland
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/programs/iay.nix b/nixos/modules/programs/iay.nix
new file mode 100644
index 00000000000..9164f5cb648
--- /dev/null
+++ b/nixos/modules/programs/iay.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.iay;
+  inherit (lib) mkEnableOption mkIf mkOption mkPackageOptionMD optionalString types;
+in {
+  options.programs.iay = {
+    enable = mkEnableOption (lib.mdDoc "iay");
+    package = mkPackageOptionMD pkgs "iay" {};
+
+    minimalPrompt = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc "Use minimal one-liner prompt.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    programs.bash.promptInit = ''
+      if [[ $TERM != "dumb" && (-z $INSIDE_EMACS || $INSIDE_EMACS == "vterm") ]]; then
+        PS1='$(iay ${optionalString cfg.minimalPrompt "-m"})'
+      fi
+    '';
+
+    programs.zsh.promptInit = ''
+      if [[ $TERM != "dumb" && (-z $INSIDE_EMACS || $INSIDE_EMACS == "vterm") ]]; then
+        autoload -Uz add-zsh-hook
+        _iay_prompt() {
+          PROMPT="$(iay -z ${optionalString cfg.minimalPrompt "-m"})"
+        }
+        add-zsh-hook precmd _iay_prompt
+      fi
+    '';
+  };
+
+  meta.maintainers = pkgs.iay.meta.maintainers;
+}
diff --git a/nixos/modules/programs/java.nix b/nixos/modules/programs/java.nix
index 4f03c1f3ff2..c5f83858d06 100644
--- a/nixos/modules/programs/java.nix
+++ b/nixos/modules/programs/java.nix
@@ -8,7 +8,6 @@ with lib;
 let
   cfg = config.programs.java;
 in
-
 {
 
   options = {
@@ -40,12 +39,35 @@ in
         type = types.package;
       };
 
+      binfmt = mkEnableOption (lib.mdDoc "binfmt to execute java jar's and classes");
+
     };
 
   };
 
   config = mkIf cfg.enable {
 
+    boot.binfmt.registrations = mkIf cfg.binfmt {
+      java-class = {
+        recognitionType = "extension";
+        magicOrExtension = "class";
+        interpreter = pkgs.writeShellScript "java-class-wrapper" ''
+          test -e ${cfg.package}/nix-support/setup-hook && source ${cfg.package}/nix-support/setup-hook
+          classpath=$(dirname "$1")
+          class=$(basename "''${1%%.class}")
+          $JAVA_HOME/bin/java -classpath "$classpath" "$class" "''${@:2}"
+        '';
+      };
+      java-jar = {
+        recognitionType = "extension";
+        magicOrExtension = "jar";
+        interpreter = pkgs.writeShellScript "java-jar-wrapper" ''
+          test -e ${cfg.package}/nix-support/setup-hook && source ${cfg.package}/nix-support/setup-hook
+          $JAVA_HOME/bin/java -jar "$@"
+        '';
+      };
+    };
+
     environment.systemPackages = [ cfg.package ];
 
     environment.shellInit = ''
diff --git a/nixos/modules/programs/k3b.nix b/nixos/modules/programs/k3b.nix
index cdaed3cf70f..5d19e4f1cc4 100644
--- a/nixos/modules/programs/k3b.nix
+++ b/nixos/modules/programs/k3b.nix
@@ -28,7 +28,7 @@ with lib;
       k3b
       dvdplusrwtools
       cdrdao
-      cdrkit
+      cdrtools
     ];
 
     security.wrappers = {
@@ -44,7 +44,7 @@ with lib;
         owner = "root";
         group = "cdrom";
         permissions = "u+wrx,g+x";
-        source = "${pkgs.cdrkit}/bin/cdrecord";
+        source = "${pkgs.cdrtools}/bin/cdrecord";
       };
     };
 
diff --git a/nixos/modules/programs/less.nix b/nixos/modules/programs/less.nix
index a1134e77436..81c68307aee 100644
--- a/nixos/modules/programs/less.nix
+++ b/nixos/modules/programs/less.nix
@@ -11,7 +11,7 @@ let
     ${concatStringsSep "\n"
       (mapAttrsToList (command: action: "${command} ${action}") cfg.commands)
     }
-    ${if cfg.clearDefaultCommands then "#stop" else ""}
+    ${optionalString cfg.clearDefaultCommands "#stop"}
 
     #line-edit
     ${concatStringsSep "\n"
diff --git a/nixos/modules/programs/minipro.nix b/nixos/modules/programs/minipro.nix
new file mode 100644
index 00000000000..a947f83f2ee
--- /dev/null
+++ b/nixos/modules/programs/minipro.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.minipro;
+in
+{
+  options = {
+    programs.minipro = {
+      enable = lib.mkEnableOption (lib.mdDoc "minipro") // {
+        description = lib.mdDoc ''
+          Installs minipro and its udev rules.
+          Users of the `plugdev` group can interact with connected MiniPRO chip programmers.
+        '';
+      };
+
+      package = lib.mkPackageOptionMD pkgs "minipro" { };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    users.groups.plugdev = { };
+    environment.systemPackages = [ cfg.package ];
+    services.udev.packages = [ cfg.package ];
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ infinidoge ];
+  };
+}
diff --git a/nixos/modules/programs/miriway.nix b/nixos/modules/programs/miriway.nix
new file mode 100644
index 00000000000..a67e1a17a7e
--- /dev/null
+++ b/nixos/modules/programs/miriway.nix
@@ -0,0 +1,78 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.programs.miriway;
+in {
+  options.programs.miriway = {
+    enable = lib.mkEnableOption (lib.mdDoc ''
+      Miriway, a Mir based Wayland compositor. You can manually launch Miriway by
+      executing "exec miriway" on a TTY, or launch it from a display manager. Copy
+      /etc/xdg/xdg-miriway/miriway-shell.config to ~/.config/miriway-shell.config
+      to modify the system-wide configuration on a per-user basis. See <https://github.com/Miriway/Miriway>,
+      and "miriway --help" for more information'');
+
+    config = lib.mkOption {
+      type = lib.types.lines;
+      default = ''
+        x11-window-title=Miriway (Mir-on-X)
+        idle-timeout=600
+        ctrl-alt=t:miriway-terminal # Default "terminal emulator finder"
+
+        shell-component=dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY
+
+        meta=Left:@dock-left
+        meta=Right:@dock-right
+        meta=Space:@toggle-maximized
+        meta=Home:@workspace-begin
+        meta=End:@workspace-end
+        meta=Page_Up:@workspace-up
+        meta=Page_Down:@workspace-down
+        ctrl-alt=BackSpace:@exit
+      '';
+      example = ''
+        idle-timeout=300
+        ctrl-alt=t:weston-terminal
+        add-wayland-extensions=all
+
+        shell-components=dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY
+
+        shell-component=waybar
+        shell-component=wbg Pictures/wallpaper
+
+        shell-meta=a:synapse
+
+        meta=Left:@dock-left
+        meta=Right:@dock-right
+        meta=Space:@toggle-maximized
+        meta=Home:@workspace-begin
+        meta=End:@workspace-end
+        meta=Page_Up:@workspace-up
+        meta=Page_Down:@workspace-down
+        ctrl-alt=BackSpace:@exit
+      '';
+      description = lib.mdDoc ''
+        Miriway's config. This will be installed system-wide.
+        The default will install the miriway package's barebones example config.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment = {
+      systemPackages = [ pkgs.miriway ];
+      etc = {
+        "xdg/xdg-miriway/miriway-shell.config".text = cfg.config;
+      };
+    };
+
+    hardware.opengl.enable = lib.mkDefault true;
+    fonts.enableDefaultFonts = lib.mkDefault true;
+    programs.dconf.enable = lib.mkDefault true;
+    programs.xwayland.enable = lib.mkDefault true;
+
+    # To make the Miriway session available if a display manager like SDDM is enabled:
+    services.xserver.displayManager.sessionPackages = [ pkgs.miriway ];
+  };
+
+  meta.maintainers = with lib.maintainers; [ OPNA2608 ];
+}
diff --git a/nixos/modules/programs/nano.nix b/nixos/modules/programs/nano.nix
index 16bab620d6e..7705bf0ddc7 100644
--- a/nixos/modules/programs/nano.nix
+++ b/nixos/modules/programs/nano.nix
@@ -35,8 +35,17 @@ in
   ###### implementation
 
   config = lib.mkIf (cfg.nanorc != "" || cfg.syntaxHighlight) {
-    environment.etc.nanorc.text = lib.concatStrings [ cfg.nanorc
-      (lib.optionalString cfg.syntaxHighlight ''${LF}include "${pkgs.nano}/share/nano/*.nanorc"'') ];
+    environment.etc.nanorc.text = lib.concatStringsSep LF (
+      ( lib.optionals cfg.syntaxHighlight [
+          "# The line below is added because value of programs.nano.syntaxHighlight is set to true"
+          ''include "${pkgs.nano}/share/nano/*.nanorc"''
+          ""
+      ])
+      ++ ( lib.optionals (cfg.nanorc != "") [
+        "# The lines below have been set from value of programs.nano.nanorc"
+        cfg.nanorc
+      ])
+    );
   };
 
 }
diff --git a/nixos/modules/programs/neovim.nix b/nixos/modules/programs/neovim.nix
index 8de527fceb2..1b53b9b5d91 100644
--- a/nixos/modules/programs/neovim.nix
+++ b/nixos/modules/programs/neovim.nix
@@ -4,12 +4,8 @@ with lib;
 
 let
   cfg = config.programs.neovim;
-
-  runtime' = filter (f: f.enable) (attrValues cfg.runtime);
-
-  runtime = pkgs.linkFarm "neovim-runtime" (map (x: { name = x.target; path = x.source; }) runtime');
-
-in {
+in
+{
   options.programs.neovim = {
     enable = mkOption {
       type = types.bool;
@@ -70,7 +66,7 @@ in {
 
     configure = mkOption {
       type = types.attrs;
-      default = {};
+      default = { };
       example = literalExpression ''
         {
           customRC = '''
@@ -105,7 +101,7 @@ in {
     };
 
     runtime = mkOption {
-      default = {};
+      default = { };
       example = literalExpression ''
         { "ftplugin/c.vim".text = "setlocal omnifunc=v:lua.vim.lsp.omnifunc"; }
       '';
@@ -115,14 +111,15 @@ in {
 
       type = with types; attrsOf (submodule (
         { name, config, ... }:
-        { options = {
+        {
+          options = {
 
             enable = mkOption {
               type = types.bool;
               default = true;
               description = lib.mdDoc ''
-                Whether this /etc file should be generated.  This
-                option allows specific /etc files to be disabled.
+                Whether this runtime directory should be generated.  This
+                option allows specific runtime files to be disabled.
               '';
             };
 
@@ -141,20 +138,16 @@ in {
             };
 
             source = mkOption {
-              type = types.path;
+              default = null;
+              type = types.nullOr types.path;
               description = lib.mdDoc "Path of the source file.";
             };
 
           };
 
-          config = {
-            target = mkDefault name;
-            source = mkIf (config.text != null) (
-              let name' = "neovim-runtime" + baseNameOf name;
-              in mkDefault (pkgs.writeText name' config.text));
-          };
-
-        }));
+          config.target = mkDefault name;
+        }
+      ));
 
     };
   };
@@ -165,14 +158,19 @@ in {
     ];
     environment.variables.EDITOR = mkIf cfg.defaultEditor (mkOverride 900 "nvim");
 
-    programs.neovim.finalPackage = pkgs.wrapNeovim cfg.package {
-      inherit (cfg) viAlias vimAlias withPython3 withNodeJs withRuby;
-      configure = cfg.configure // {
+    environment.etc = listToAttrs (attrValues (mapAttrs
+      (name: value: {
+        name = "xdg/nvim/${name}";
+        value = removeAttrs
+          (value // {
+            target = "xdg/nvim/${value.target}";
+          })
+          (optionals (isNull value.source) [ "source" ]);
+      })
+      cfg.runtime));
 
-        customRC = (cfg.configure.customRC or "") + ''
-          set runtimepath^=${runtime}/etc
-        '';
-      };
+    programs.neovim.finalPackage = pkgs.wrapNeovim cfg.package {
+      inherit (cfg) viAlias vimAlias withPython3 withNodeJs withRuby configure;
     };
   };
 }
diff --git a/nixos/modules/programs/nexttrace.nix b/nixos/modules/programs/nexttrace.nix
new file mode 100644
index 00000000000..091d4f17f9f
--- /dev/null
+++ b/nixos/modules/programs/nexttrace.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.nexttrace;
+
+in
+{
+  options = {
+    programs.nexttrace = {
+      enable = lib.mkEnableOption (lib.mdDoc "Nexttrace to the global environment and configure a setcap wrapper for it");
+      package = lib.mkPackageOptionMD pkgs "nexttrace" { };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    security.wrappers.nexttrace = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_raw,cap_net_admin+eip";
+      source = "${cfg.package}/bin/nexttrace";
+    };
+  };
+}
diff --git a/nixos/modules/programs/nix-ld.nix b/nixos/modules/programs/nix-ld.nix
index 9a12b4ca5c7..d54b3917f89 100644
--- a/nixos/modules/programs/nix-ld.nix
+++ b/nixos/modules/programs/nix-ld.nix
@@ -2,15 +2,14 @@
 let
   cfg = config.programs.nix-ld;
 
-  # TODO make glibc here configureable?
-  nix-ld-so = pkgs.runCommand "ld.so" {} ''
-    ln -s "$(cat '${pkgs.stdenv.cc}/nix-support/dynamic-linker')" $out
-  '';
-
   nix-ld-libraries = pkgs.buildEnv {
     name = "lb-library-path";
     pathsToLink = [ "/lib" ];
     paths = map lib.getLib cfg.libraries;
+    # TODO make glibc here configurable?
+    postBuild = ''
+      ln -s ${pkgs.stdenv.cc.bintools.dynamicLinker} $out/share/nix-ld/lib/ld.so
+    '';
     extraPrefix = "/share/nix-ld";
     ignoreCollisions = true;
   };
@@ -38,12 +37,7 @@ in
   meta.maintainers = [ lib.maintainers.mic92 ];
   options.programs.nix-ld = {
     enable = lib.mkEnableOption (lib.mdDoc ''nix-ld, Documentation: <https://github.com/Mic92/nix-ld>'');
-    package = lib.mkOption {
-      type = lib.types.package;
-      description = lib.mdDoc "Which package to use for the nix-ld.";
-      default = pkgs.nix-ld;
-      defaultText = lib.literalExpression "pkgs.nix-ld";
-    };
+    package = lib.mkPackageOptionMD pkgs "nix-ld" { };
     libraries = lib.mkOption {
       type = lib.types.listOf lib.types.package;
       description = lib.mdDoc "Libraries that automatically become available to all programs. The default set includes common libraries.";
@@ -60,7 +54,7 @@ in
     environment.pathsToLink = [ "/share/nix-ld" ];
 
     environment.variables = {
-      NIX_LD = toString nix-ld-so;
+      NIX_LD = "/run/current-system/sw/share/nix-ld/lib/ld.so";
       NIX_LD_LIBRARY_PATH = "/run/current-system/sw/share/nix-ld/lib";
     };
   };
diff --git a/nixos/modules/programs/plotinus.md b/nixos/modules/programs/plotinus.md
new file mode 100644
index 00000000000..fac3bbad1e0
--- /dev/null
+++ b/nixos/modules/programs/plotinus.md
@@ -0,0 +1,17 @@
+# Plotinus {#module-program-plotinus}
+
+*Source:* {file}`modules/programs/plotinus.nix`
+
+*Upstream documentation:* <https://github.com/p-e-w/plotinus>
+
+Plotinus is a searchable command palette in every modern GTK application.
+
+When in a GTK 3 application and Plotinus is enabled, you can press
+`Ctrl+Shift+P` to open the command palette. The command
+palette provides a searchable list of of all menu items in the application.
+
+To enable Plotinus, add the following to your
+{file}`configuration.nix`:
+```
+programs.plotinus.enable = true;
+```
diff --git a/nixos/modules/programs/plotinus.nix b/nixos/modules/programs/plotinus.nix
index a011bb862ae..c2b6884d649 100644
--- a/nixos/modules/programs/plotinus.nix
+++ b/nixos/modules/programs/plotinus.nix
@@ -8,7 +8,7 @@ in
 {
   meta = {
     maintainers = pkgs.plotinus.meta.maintainers;
-    doc = ./plotinus.xml;
+    doc = ./plotinus.md;
   };
 
   ###### interface
diff --git a/nixos/modules/programs/plotinus.xml b/nixos/modules/programs/plotinus.xml
deleted file mode 100644
index 8fc8c22c6d7..00000000000
--- a/nixos/modules/programs/plotinus.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-program-plotinus">
- <title>Plotinus</title>
- <para>
-  <emphasis>Source:</emphasis>
-  <filename>modules/programs/plotinus.nix</filename>
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="https://github.com/p-e-w/plotinus"/>
- </para>
- <para>
-  Plotinus is a searchable command palette in every modern GTK application.
- </para>
- <para>
-  When in a GTK 3 application and Plotinus is enabled, you can press
-  <literal>Ctrl+Shift+P</literal> to open the command palette. The command
-  palette provides a searchable list of of all menu items in the application.
- </para>
- <para>
-  To enable Plotinus, add the following to your
-  <filename>configuration.nix</filename>:
-<programlisting>
-<xref linkend="opt-programs.plotinus.enable"/> = true;
-</programlisting>
- </para>
-</chapter>
diff --git a/nixos/modules/programs/proxychains.nix b/nixos/modules/programs/proxychains.nix
index 0771f03c77d..9bdd5d40566 100644
--- a/nixos/modules/programs/proxychains.nix
+++ b/nixos/modules/programs/proxychains.nix
@@ -51,6 +51,10 @@ in {
 
       enable = mkEnableOption (lib.mdDoc "installing proxychains configuration");
 
+      package = mkPackageOptionMD pkgs "proxychains" {
+        example = "pkgs.proxychains-ng";
+      };
+
       chain = {
         type = mkOption {
           type = types.enum [ "dynamic" "strict" "random" ];
@@ -86,7 +90,7 @@ in {
         description = lib.mdDoc "Proxy DNS requests - no leak for DNS data.";
       };
 
-      quietMode = mkEnableOption (lib.mdDoc "Quiet mode (no output from the library).");
+      quietMode = mkEnableOption (lib.mdDoc "Quiet mode (no output from the library)");
 
       remoteDNSSubnet = mkOption {
         type = types.enum [ 10 127 224 ];
@@ -159,7 +163,7 @@ in {
       };
 
     environment.etc."proxychains.conf".text = configFile;
-    environment.systemPackages = [ pkgs.proxychains ];
+    environment.systemPackages = [ cfg.package ];
   };
 
 }
diff --git a/nixos/modules/programs/qdmr.nix b/nixos/modules/programs/qdmr.nix
new file mode 100644
index 00000000000..1bb81317bda
--- /dev/null
+++ b/nixos/modules/programs/qdmr.nix
@@ -0,0 +1,25 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+
+let
+  cfg = config.programs.qdmr;
+in {
+  meta.maintainers = [ lib.maintainers.janik ];
+
+  options = {
+    programs.qdmr = {
+      enable = lib.mkEnableOption (lib.mdDoc "QDMR - a GUI application and command line tool for programming DMR radios");
+      package = lib.mkPackageOptionMD pkgs "qdmr" { };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    services.udev.packages = [ cfg.package ];
+    users.groups.dialout = {};
+  };
+}
diff --git a/nixos/modules/programs/regreet.nix b/nixos/modules/programs/regreet.nix
new file mode 100644
index 00000000000..f6c750a45bf
--- /dev/null
+++ b/nixos/modules/programs/regreet.nix
@@ -0,0 +1,75 @@
+{ lib
+, pkgs
+, config
+, ...
+}:
+let
+  cfg = config.programs.regreet;
+  settingsFormat = pkgs.formats.toml { };
+in
+{
+  options.programs.regreet = {
+    enable = lib.mkEnableOption null // {
+      description = lib.mdDoc ''
+        Enable ReGreet, a clean and customizable greeter for greetd.
+
+        To use ReGreet, {option}`services.greetd` has to be enabled and
+        {option}`services.greetd.settings.default_session` should contain the
+        appropriate configuration to launch
+        {option}`config.programs.regreet.package`. For examples, see the
+        [ReGreet Readme](https://github.com/rharish101/ReGreet#set-as-default-session).
+
+        A minimal configuration that launches ReGreet in {command}`cage` is
+        enabled by this module by default.
+      '';
+    };
+
+    package = lib.mkPackageOptionMD pkgs [ "greetd" "regreet" ] { };
+
+    settings = lib.mkOption {
+      type = lib.types.either lib.types.path settingsFormat.type;
+      default = { };
+      description = lib.mdDoc ''
+        ReGreet configuration file. Refer
+        <https://github.com/rharish101/ReGreet/blob/main/regreet.sample.toml>
+        for options.
+      '';
+    };
+
+    extraCss = lib.mkOption {
+      type = lib.types.either lib.types.path lib.types.lines;
+      default = "";
+      description = lib.mdDoc ''
+        Extra CSS rules to apply on top of the GTK theme. Refer to
+        [GTK CSS Properties](https://docs.gtk.org/gtk4/css-properties.html) for
+        modifiable properties.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.greetd = {
+      enable = lib.mkDefault true;
+      settings.default_session.command = lib.mkDefault "${pkgs.dbus}/bin/dbus-run-session ${lib.getExe pkgs.cage} -s -- ${lib.getExe cfg.package}";
+    };
+
+    environment.etc = {
+      "greetd/regreet.css" =
+        if lib.isPath cfg.extraCss
+        then {source = cfg.extraCss;}
+        else {text = cfg.extraCss;};
+
+      "greetd/regreet.toml".source =
+        if lib.isPath cfg.settings
+        then cfg.settings
+        else settingsFormat.generate "regreet.toml" cfg.settings;
+    };
+
+    systemd.tmpfiles.rules = let
+      user = config.services.greetd.settings.default_session.user;
+    in [
+      "d /var/log/regreet 0755 greeter ${user} - -"
+      "d /var/cache/regreet 0755 greeter ${user} - -"
+    ];
+  };
+}
diff --git a/nixos/modules/programs/shadow.nix b/nixos/modules/programs/shadow.nix
index fab809f279a..00895db03fc 100644
--- a/nixos/modules/programs/shadow.nix
+++ b/nixos/modules/programs/shadow.nix
@@ -1,65 +1,131 @@
 # Configuration for the pwdutils suite of tools: passwd, useradd, etc.
-
 { config, lib, utils, pkgs, ... }:
-
 with lib;
-
 let
-
-  /*
-  There are three different sources for user/group id ranges, each of which gets
-  used by different programs:
-  - The login.defs file, used by the useradd, groupadd and newusers commands
-  - The update-users-groups.pl file, used by NixOS in the activation phase to
-    decide on which ids to use for declaratively defined users without a static
-    id
-  - Systemd compile time options -Dsystem-uid-max= and -Dsystem-gid-max=, used
-    by systemd for features like ConditionUser=@system and systemd-sysusers
-  */
-  loginDefs =
-    ''
-      DEFAULT_HOME yes
-
-      SYS_UID_MIN  400
-      SYS_UID_MAX  999
-      UID_MIN      1000
-      UID_MAX      29999
-
-      SYS_GID_MIN  400
-      SYS_GID_MAX  999
-      GID_MIN      1000
-      GID_MAX      29999
-
-      TTYGROUP     tty
-      TTYPERM      0620
-
-      # Ensure privacy for newly created home directories.
-      UMASK        077
-
-      # Uncomment this and install chfn SUID to allow non-root
-      # users to change their account GECOS information.
-      # This should be made configurable.
-      #CHFN_RESTRICT frwh
-
-    '';
-
-  mkSetuidRoot = source:
-    { setuid = true;
-      owner = "root";
-      group = "root";
-      inherit source;
-    };
-
+  cfg = config.security.loginDefs;
 in
-
 {
+  options = with types; {
+    security.loginDefs = {
+      package = mkPackageOptionMD pkgs "shadow" { };
+
+      chfnRestrict = mkOption {
+        description = mdDoc ''
+          Use chfn SUID to allow non-root users to change their account GECOS information.
+        '';
+        type = nullOr str;
+        default = null;
+      };
 
-  ###### interface
-
-  options = {
+      settings = mkOption {
+        description = mdDoc ''
+          Config options for the /etc/login.defs file, that defines
+          the site-specific configuration for the shadow password suite.
+          See login.defs(5) man page for available options.
+        '';
+        type = submodule {
+          freeformType = (pkgs.formats.keyValue { }).type;
+          /* There are three different sources for user/group id ranges, each of which gets
+             used by different programs:
+             - The login.defs file, used by the useradd, groupadd and newusers commands
+             - The update-users-groups.pl file, used by NixOS in the activation phase to
+               decide on which ids to use for declaratively defined users without a static
+               id
+             - Systemd compile time options -Dsystem-uid-max= and -Dsystem-gid-max=, used
+               by systemd for features like ConditionUser=@system and systemd-sysusers
+              */
+          options = {
+            DEFAULT_HOME = mkOption {
+              description = mdDoc "Indicate if login is allowed if we can't cd to the home directory.";
+              default = "yes";
+              type = enum [ "yes" "no" ];
+            };
+
+            ENCRYPT_METHOD = mkOption {
+              description = mdDoc "This defines the system default encryption algorithm for encrypting passwords.";
+              # The default crypt() method, keep in sync with the PAM default
+              default = "YESCRYPT";
+              type = enum [ "YESCRYPT" "SHA512" "SHA256" "MD5" "DES"];
+            };
+
+            SYS_UID_MIN = mkOption {
+              description = mdDoc "Range of user IDs used for the creation of system users by useradd or newusers.";
+              default = 400;
+              type = int;
+            };
+
+            SYS_UID_MAX = mkOption {
+              description = mdDoc "Range of user IDs used for the creation of system users by useradd or newusers.";
+              default = 999;
+              type = int;
+            };
+
+            UID_MIN = mkOption {
+              description = mdDoc "Range of user IDs used for the creation of regular users by useradd or newusers.";
+              default = 1000;
+              type = int;
+            };
+
+            UID_MAX = mkOption {
+              description = mdDoc "Range of user IDs used for the creation of regular users by useradd or newusers.";
+              default = 29999;
+              type = int;
+            };
+
+            SYS_GID_MIN = mkOption {
+              description = mdDoc "Range of group IDs used for the creation of system groups by useradd, groupadd, or newusers";
+              default = 400;
+              type = int;
+            };
+
+            SYS_GID_MAX = mkOption {
+              description = mdDoc "Range of group IDs used for the creation of system groups by useradd, groupadd, or newusers";
+              default = 999;
+              type = int;
+            };
+
+            GID_MIN = mkOption {
+              description = mdDoc "Range of group IDs used for the creation of regular groups by useradd, groupadd, or newusers.";
+              default = 1000;
+              type = int;
+            };
+
+            GID_MAX = mkOption {
+              description = mdDoc "Range of group IDs used for the creation of regular groups by useradd, groupadd, or newusers.";
+              default = 29999;
+              type = int;
+            };
+
+            TTYGROUP = mkOption {
+              description = mdDoc ''
+                The terminal permissions: the login tty will be owned by the TTYGROUP group,
+                and the permissions will be set to TTYPERM'';
+              default = "tty";
+              type = str;
+            };
+
+            TTYPERM = mkOption {
+              description = mdDoc ''
+                The terminal permissions: the login tty will be owned by the TTYGROUP group,
+                and the permissions will be set to TTYPERM'';
+              default = "0620";
+              type = str;
+            };
+
+            # Ensure privacy for newly created home directories.
+            UMASK = mkOption {
+              description = mdDoc "The file mode creation mask is initialized to this value.";
+              default = "077";
+              type = str;
+            };
+          };
+        };
+        default = { };
+      };
+    };
 
-    users.defaultUserShell = lib.mkOption {
-      description = lib.mdDoc ''
+    users.defaultUserShell = mkOption {
+      description = mdDoc ''
         This option defines the default shell assigned to user
         accounts. This can be either a full system path or a shell package.
 
@@ -67,63 +133,107 @@ in
         used outside the store (in particular in /etc/passwd).
       '';
       example = literalExpression "pkgs.zsh";
-      type = types.either types.path types.shellPackage;
+      type = either path shellPackage;
     };
-
   };
 
-
   ###### implementation
 
   config = {
-
-    environment.systemPackages =
-      lib.optional config.users.mutableUsers pkgs.shadow ++
-      lib.optional (types.shellPackage.check config.users.defaultUserShell)
-        config.users.defaultUserShell;
+    assertions = [
+      {
+        assertion = cfg.settings.SYS_UID_MIN <= cfg.settings.SYS_UID_MAX;
+        message = "SYS_UID_MIN must be less than or equal to SYS_UID_MAX";
+      }
+      {
+        assertion = cfg.settings.UID_MIN <= cfg.settings.UID_MAX;
+        message = "UID_MIN must be less than or equal to UID_MAX";
+      }
+      {
+        assertion = cfg.settings.SYS_GID_MIN <= cfg.settings.SYS_GID_MAX;
+        message = "SYS_GID_MIN must be less than or equal to SYS_GID_MAX";
+      }
+      {
+        assertion = cfg.settings.GID_MIN <= cfg.settings.GID_MAX;
+        message = "GID_MIN must be less than or equal to GID_MAX";
+      }
+    ];
+
+    security.loginDefs.settings.CHFN_RESTRICT =
+      mkIf (cfg.chfnRestrict != null) cfg.chfnRestrict;
+
+    environment.systemPackages = optional config.users.mutableUsers cfg.package
+      ++ optional (types.shellPackage.check config.users.defaultUserShell) config.users.defaultUserShell
+      ++ optional (cfg.chfnRestrict != null) pkgs.util-linux;
 
     environment.etc =
-      { # /etc/login.defs: global configuration for pwdutils.  You
-        # cannot login without it!
-        "login.defs".source = pkgs.writeText "login.defs" loginDefs;
+      # Create custom toKeyValue generator
+      # see https://man7.org/linux/man-pages/man5/login.defs.5.html for config specification
+      let
+        toKeyValue = generators.toKeyValue {
+          mkKeyValue = generators.mkKeyValueDefault { } " ";
+        };
+      in
+      {
+        # /etc/login.defs: global configuration for pwdutils.
+        # You cannot login without it!
+        "login.defs".source = pkgs.writeText "login.defs" (toKeyValue cfg.settings);
 
         # /etc/default/useradd: configuration for useradd.
-        "default/useradd".source = pkgs.writeText "useradd"
-          ''
-            GROUP=100
-            HOME=/home
-            SHELL=${utils.toShellPath config.users.defaultUserShell}
-          '';
+        "default/useradd".source = pkgs.writeText "useradd" ''
+          GROUP=100
+          HOME=/home
+          SHELL=${utils.toShellPath config.users.defaultUserShell}
+        '';
       };
 
-    security.pam.services =
-      { chsh = { rootOK = true; };
-        chfn = { rootOK = true; };
-        su = { rootOK = true; forwardXAuth = true; logFailures = true; };
-        passwd = {};
-        # Note: useradd, groupadd etc. aren't setuid root, so it
-        # doesn't really matter what the PAM config says as long as it
-        # lets root in.
-        useradd = { rootOK = true; };
-        usermod = { rootOK = true; };
-        userdel = { rootOK = true; };
-        groupadd = { rootOK = true; };
-        groupmod = { rootOK = true; };
-        groupmems = { rootOK = true; };
-        groupdel = { rootOK = true; };
-        login = { startSession = true; allowNullPassword = true; showMotd = true; updateWtmp = true; };
-        chpasswd = { rootOK = true; };
+    security.pam.services = {
+      chsh = { rootOK = true; };
+      chfn = { rootOK = true; };
+      su = {
+        rootOK = true;
+        forwardXAuth = true;
+        logFailures = true;
       };
-
-    security.wrappers = {
-      su        = mkSetuidRoot "${pkgs.shadow.su}/bin/su";
-      sg        = mkSetuidRoot "${pkgs.shadow.out}/bin/sg";
-      newgrp    = mkSetuidRoot "${pkgs.shadow.out}/bin/newgrp";
-      newuidmap = mkSetuidRoot "${pkgs.shadow.out}/bin/newuidmap";
-      newgidmap = mkSetuidRoot "${pkgs.shadow.out}/bin/newgidmap";
-    } // lib.optionalAttrs config.users.mutableUsers {
-      chsh   = mkSetuidRoot "${pkgs.shadow.out}/bin/chsh";
-      passwd = mkSetuidRoot "${pkgs.shadow.out}/bin/passwd";
+      passwd = { };
+      # Note: useradd, groupadd etc. aren't setuid root, so it
+      # doesn't really matter what the PAM config says as long as it
+      # lets root in.
+      useradd.rootOK = true;
+      usermod.rootOK = true;
+      userdel.rootOK = true;
+      groupadd.rootOK = true;
+      groupmod.rootOK = true;
+      groupmems.rootOK = true;
+      groupdel.rootOK = true;
+      login = {
+        startSession = true;
+        allowNullPassword = true;
+        showMotd = true;
+        updateWtmp = true;
+      };
+      chpasswd = { rootOK = true; };
     };
+
+    security.wrappers =
+      let
+        mkSetuidRoot = source: {
+          setuid = true;
+          owner = "root";
+          group = "root";
+          inherit source;
+        };
+      in
+      {
+        su = mkSetuidRoot "${cfg.package.su}/bin/su";
+        sg = mkSetuidRoot "${cfg.package.out}/bin/sg";
+        newgrp = mkSetuidRoot "${cfg.package.out}/bin/newgrp";
+        newuidmap = mkSetuidRoot "${cfg.package.out}/bin/newuidmap";
+        newgidmap = mkSetuidRoot "${cfg.package.out}/bin/newgidmap";
+      }
+      // optionalAttrs config.users.mutableUsers {
+        chsh = mkSetuidRoot "${cfg.package.out}/bin/chsh";
+        passwd = mkSetuidRoot "${cfg.package.out}/bin/passwd";
+      };
   };
 }
diff --git a/nixos/modules/programs/sharing.nix b/nixos/modules/programs/sharing.nix
new file mode 100644
index 00000000000..9ab51859dc5
--- /dev/null
+++ b/nixos/modules/programs/sharing.nix
@@ -0,0 +1,19 @@
+{ config, pkgs, lib, ... }:
+with lib;
+{
+  options.programs.sharing = {
+    enable = mkEnableOption (lib.mdDoc ''
+      sharing, a CLI tool for sharing files.
+
+      Note that it will opens the 7478 port for TCP in the firewall, which is needed for it to function properly
+    '');
+  };
+  config =
+    let
+      cfg = config.programs.sharing;
+    in
+      mkIf cfg.enable {
+        environment.systemPackages = [ pkgs.sharing ];
+        networking.firewall.allowedTCPPorts = [ 7478 ];
+      };
+}
diff --git a/nixos/modules/programs/singularity.nix b/nixos/modules/programs/singularity.nix
index 9648d0c2787..05fdb4842c5 100644
--- a/nixos/modules/programs/singularity.nix
+++ b/nixos/modules/programs/singularity.nix
@@ -3,32 +3,90 @@
 with lib;
 let
   cfg = config.programs.singularity;
-  singularity = pkgs.singularity.overrideAttrs (attrs : {
-    installPhase = attrs.installPhase + ''
-      mv $out/libexec/singularity/bin/starter-suid $out/libexec/singularity/bin/starter-suid.orig
-      ln -s /run/wrappers/bin/singularity-suid $out/libexec/singularity/bin/starter-suid
-    '';
-  });
-in {
+in
+{
+
   options.programs.singularity = {
-    enable = mkEnableOption (lib.mdDoc "Singularity");
+    enable = mkEnableOption (mdDoc "singularity") // {
+      description = mdDoc ''
+        Whether to install Singularity/Apptainer with system-level overriding such as SUID support.
+      '';
+    };
+    package = mkOption {
+      type = types.package;
+      default = pkgs.singularity;
+      defaultText = literalExpression "pkgs.singularity";
+      example = literalExpression "pkgs.apptainer";
+      description = mdDoc ''
+        Singularity/Apptainer package to override and install.
+      '';
+    };
+    packageOverriden = mkOption {
+      type = types.nullOr types.package;
+      default = null;
+      description = mdDoc ''
+        This option provides access to the overridden result of `programs.singularity.package`.
+
+        For example, the following configuration makes all the Nixpkgs packages use the overridden `singularity`:
+        ```Nix
+        { config, lib, pkgs, ... }:
+        {
+          nixpkgs.overlays = [
+            (final: prev: {
+              _singularity-orig = prev.singularity;
+              singularity = config.programs.singularity.packageOverriden;
+            })
+          ];
+          programs.singularity.enable = true;
+          programs.singularity.package = pkgs._singularity-orig;
+        }
+        ```
+
+        Use `lib.mkForce` to forcefully specify the overridden package.
+      '';
+    };
+    enableFakeroot = mkOption {
+      type = types.bool;
+      default = true;
+      example = false;
+      description = mdDoc ''
+        Whether to enable the `--fakeroot` support of Singularity/Apptainer.
+      '';
+    };
+    enableSuid = mkOption {
+      type = types.bool;
+      default = true;
+      example = false;
+      description = mdDoc ''
+        Whether to enable the SUID support of Singularity/Apptainer.
+      '';
+    };
   };
 
   config = mkIf cfg.enable {
-      environment.systemPackages = [ singularity ];
-      security.wrappers.singularity-suid =
-      { setuid = true;
-        owner = "root";
-        group = "root";
-        source = "${singularity}/libexec/singularity/bin/starter-suid.orig";
-      };
-      systemd.tmpfiles.rules = [
-        "d /var/singularity/mnt/session 0770 root root -"
-        "d /var/singularity/mnt/final 0770 root root -"
-        "d /var/singularity/mnt/overlay 0770 root root -"
-        "d /var/singularity/mnt/container 0770 root root -"
-        "d /var/singularity/mnt/source 0770 root root -"
-      ];
+    programs.singularity.packageOverriden = (cfg.package.override (
+      optionalAttrs cfg.enableFakeroot {
+        newuidmapPath = "/run/wrappers/bin/newuidmap";
+        newgidmapPath = "/run/wrappers/bin/newgidmap";
+      } // optionalAttrs cfg.enableSuid {
+        enableSuid = true;
+        starterSuidPath = "/run/wrappers/bin/${cfg.package.projectName}-suid";
+      }
+    ));
+    environment.systemPackages = [ cfg.packageOverriden ];
+    security.wrappers."${cfg.packageOverriden.projectName}-suid" = mkIf cfg.enableSuid {
+      setuid = true;
+      owner = "root";
+      group = "root";
+      source = "${cfg.packageOverriden}/libexec/${cfg.packageOverriden.projectName}/bin/starter-suid.orig";
+    };
+    systemd.tmpfiles.rules = [
+      "d /var/lib/${cfg.packageOverriden.projectName}/mnt/session 0770 root root -"
+      "d /var/lib/${cfg.packageOverriden.projectName}/mnt/final 0770 root root -"
+      "d /var/lib/${cfg.packageOverriden.projectName}/mnt/overlay 0770 root root -"
+      "d /var/lib/${cfg.packageOverriden.projectName}/mnt/container 0770 root root -"
+      "d /var/lib/${cfg.packageOverriden.projectName}/mnt/source 0770 root root -"
+    ];
   };
 
 }
diff --git a/nixos/modules/programs/skim.nix b/nixos/modules/programs/skim.nix
index 57a5d68ec3d..8dadf322606 100644
--- a/nixos/modules/programs/skim.nix
+++ b/nixos/modules/programs/skim.nix
@@ -1,6 +1,6 @@
 { pkgs, config, lib, ... }:
 let
-  inherit (lib) mdDoc mkEnableOption mkPackageOption optional optionalString;
+  inherit (lib) mdDoc mkEnableOption mkPackageOptionMD optional optionalString;
   cfg = config.programs.skim;
 in
 {
@@ -8,7 +8,7 @@ in
     programs.skim = {
       fuzzyCompletion = mkEnableOption (mdDoc "fuzzy completion with skim");
       keybindings = mkEnableOption (mdDoc "skim keybindings");
-      package = mkPackageOption pkgs "skim" {};
+      package = mkPackageOptionMD pkgs "skim" {};
     };
   };
 
diff --git a/nixos/modules/programs/sniffnet.nix b/nixos/modules/programs/sniffnet.nix
new file mode 100644
index 00000000000..98e9f628a9b
--- /dev/null
+++ b/nixos/modules/programs/sniffnet.nix
@@ -0,0 +1,24 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.sniffnet;
+in
+
+{
+  options = {
+    programs.sniffnet = {
+      enable = lib.mkEnableOption (lib.mdDoc "sniffnet");
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    security.wrappers.sniffnet = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_raw,cap_net_admin=eip";
+      source = "${pkgs.sniffnet}/bin/sniffnet";
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ figsoda ];
+}
diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix
index 36b724e04bd..7c85d1e7c3d 100644
--- a/nixos/modules/programs/ssh.nix
+++ b/nixos/modules/programs/ssh.nix
@@ -26,7 +26,7 @@ let
       + (if h.publicKey != null then h.publicKey else readFile h.publicKeyFile)
     )) + "\n";
 
-  knownHostsFiles = [ "/etc/ssh/ssh_known_hosts" "/etc/ssh/ssh_known_hosts2" ]
+  knownHostsFiles = [ "/etc/ssh/ssh_known_hosts" ]
     ++ map pkgs.copyPathToStore cfg.knownHostsFiles;
 
 in
@@ -232,15 +232,14 @@ in
         description = lib.mdDoc ''
           Files containing SSH host keys to set as global known hosts.
           `/etc/ssh/ssh_known_hosts` (which is
-          generated by {option}`programs.ssh.knownHosts`) and
-          `/etc/ssh/ssh_known_hosts2` are always
-          included.
+          generated by {option}`programs.ssh.knownHosts`) is
+          always included.
         '';
         example = literalExpression ''
           [
             ./known_hosts
             (writeText "github.keys" '''
-              github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+              github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
               github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
               github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
             ''')
@@ -282,7 +281,7 @@ in
   config = {
 
     programs.ssh.setXAuthLocation =
-      mkDefault (config.services.xserver.enable || config.programs.ssh.forwardX11 || config.services.openssh.forwardX11);
+      mkDefault (config.services.xserver.enable || config.programs.ssh.forwardX11 || config.services.openssh.settings.X11Forwarding);
 
     assertions =
       [ { assertion = cfg.forwardX11 -> cfg.setXAuthLocation;
diff --git a/nixos/modules/programs/starship.nix b/nixos/modules/programs/starship.nix
index b56c0b25616..9dca39da5ed 100644
--- a/nixos/modules/programs/starship.nix
+++ b/nixos/modules/programs/starship.nix
@@ -9,10 +9,27 @@ let
 
   settingsFile = settingsFormat.generate "starship.toml" cfg.settings;
 
-in {
+  initOption =
+    if cfg.interactiveOnly then
+      "promptInit"
+    else
+      "shellInit";
+
+in
+{
   options.programs.starship = {
     enable = mkEnableOption (lib.mdDoc "the Starship shell prompt");
 
+    interactiveOnly = mkOption {
+      default = true;
+      example = false;
+      type = types.bool;
+      description = lib.mdDoc ''
+        Whether to enable starship only when the shell is interactive.
+        Some plugins require this to be set to false to function correctly.
+      '';
+    };
+
     settings = mkOption {
       inherit (settingsFormat) type;
       default = { };
@@ -25,22 +42,22 @@ in {
   };
 
   config = mkIf cfg.enable {
-    programs.bash.promptInit = ''
-      if [[ $TERM != "dumb" && (-z $INSIDE_EMACS || $INSIDE_EMACS == "vterm") ]]; then
+    programs.bash.${initOption} = ''
+      if [[ $TERM != "dumb" ]]; then
         export STARSHIP_CONFIG=${settingsFile}
         eval "$(${pkgs.starship}/bin/starship init bash)"
       fi
     '';
 
-    programs.fish.promptInit = ''
-      if test "$TERM" != "dumb" -a \( -z "$INSIDE_EMACS" -o "$INSIDE_EMACS" = "vterm" \)
+    programs.fish.${initOption} = ''
+      if test "$TERM" != "dumb"
         set -x STARSHIP_CONFIG ${settingsFile}
         eval (${pkgs.starship}/bin/starship init fish)
       end
     '';
 
-    programs.zsh.promptInit = ''
-      if [[ $TERM != "dumb" && (-z $INSIDE_EMACS || $INSIDE_EMACS == "vterm") ]]; then
+    programs.zsh.${initOption} = ''
+      if [[ $TERM != "dumb" ]]; then
         export STARSHIP_CONFIG=${settingsFile}
         eval "$(${pkgs.starship}/bin/starship init zsh)"
       fi
diff --git a/nixos/modules/programs/steam.nix b/nixos/modules/programs/steam.nix
index 1b69aac9886..c63b31bde11 100644
--- a/nixos/modules/programs/steam.nix
+++ b/nixos/modules/programs/steam.nix
@@ -4,28 +4,65 @@ with lib;
 
 let
   cfg = config.programs.steam;
+  gamescopeCfg = config.programs.gamescope;
+
+  steam-gamescope = let
+    exports = builtins.attrValues (builtins.mapAttrs (n: v: "export ${n}=${v}") cfg.gamescopeSession.env);
+  in
+    pkgs.writeShellScriptBin "steam-gamescope" ''
+      ${builtins.concatStringsSep "\n" exports}
+      gamescope --steam ${toString cfg.gamescopeSession.args} -- steam -tenfoot -pipewire-dmabuf
+    '';
+
+  gamescopeSessionFile =
+    (pkgs.writeTextDir "share/wayland-sessions/steam.desktop" ''
+      [Desktop Entry]
+      Name=Steam
+      Comment=A digital distribution platform
+      Exec=${steam-gamescope}/bin/steam-gamescope
+      Type=Application
+    '').overrideAttrs (_: { passthru.providedSessions = [ "steam" ]; });
 in {
   options.programs.steam = {
     enable = mkEnableOption (lib.mdDoc "steam");
 
     package = mkOption {
-      type        = types.package;
-      default     = pkgs.steam.override {
-        extraLibraries = pkgs: with config.hardware.opengl;
-          if pkgs.hostPlatform.is64bit
-          then [ package ] ++ extraPackages
-          else [ package32 ] ++ extraPackages32;
-      };
-      defaultText = literalExpression ''
-        pkgs.steam.override {
-          extraLibraries = pkgs: with config.hardware.opengl;
-            if pkgs.hostPlatform.is64bit
-            then [ package ] ++ extraPackages
-            else [ package32 ] ++ extraPackages32;
+      type = types.package;
+      default = pkgs.steam;
+      defaultText = literalExpression "pkgs.steam";
+      example = literalExpression ''
+        pkgs.steam-small.override {
+          extraEnv = {
+            MANGOHUD = true;
+            OBS_VKCAPTURE = true;
+            RADV_TEX_ANISO = 16;
+          };
+          extraLibraries = p: with p; [
+            atk
+          ];
         }
       '';
+      apply = steam: steam.override (prev: {
+        extraLibraries = pkgs: let
+          prevLibs = if prev ? extraLibraries then prev.extraLibraries pkgs else [ ];
+          additionalLibs = with config.hardware.opengl;
+            if pkgs.stdenv.hostPlatform.is64bit
+            then [ package ] ++ extraPackages
+            else [ package32 ] ++ extraPackages32;
+        in prevLibs ++ additionalLibs;
+      } // optionalAttrs (cfg.gamescopeSession.enable && gamescopeCfg.capSysNice)
+      {
+        buildFHSEnv = pkgs.buildFHSEnv.override {
+          # use the setuid wrapped bubblewrap
+          bubblewrap = "${config.security.wrapperDir}/..";
+        };
+      });
       description = lib.mdDoc ''
-        steam package to use.
+        The Steam package to use. Additional libraries are added from the system
+        configuration to ensure graphics work properly.
+
+        Use this option to customise the Steam package rather than adding your
+        custom Steam to {option}`environment.systemPackages` yourself.
       '';
     };
 
@@ -44,6 +81,31 @@ in {
         Open ports in the firewall for Source Dedicated Server.
       '';
     };
+
+    gamescopeSession = mkOption {
+      description = mdDoc "Run a GameScope driven Steam session from your display-manager";
+      default = {};
+      type = types.submodule {
+        options = {
+          enable = mkEnableOption (mdDoc "GameScope Session");
+          args = mkOption {
+            type = types.listOf types.string;
+            default = [ ];
+            description = mdDoc ''
+              Arguments to be passed to GameScope for the session.
+            '';
+          };
+
+          env = mkOption {
+            type = types.attrsOf types.string;
+            default = { };
+            description = mdDoc ''
+              Environmental variables to be passed to GameScope for the session.
+            '';
+          };
+        };
+      };
+    };
   };
 
   config = mkIf cfg.enable {
@@ -53,6 +115,19 @@ in {
       driSupport32Bit = true;
     };
 
+    security.wrappers = mkIf (cfg.gamescopeSession.enable && gamescopeCfg.capSysNice) {
+      # needed or steam fails
+      bwrap = {
+        owner = "root";
+        group = "root";
+        source = "${pkgs.bubblewrap}/bin/bwrap";
+        setuid = true;
+      };
+    };
+
+    programs.gamescope.enable = mkDefault cfg.gamescopeSession.enable;
+    services.xserver.displayManager.sessionPackages = mkIf cfg.gamescopeSession.enable [ gamescopeSessionFile ];
+
     # optionally enable 32bit pulseaudio support if pulseaudio is enabled
     hardware.pulseaudio.support32Bit = config.hardware.pulseaudio.enable;
 
@@ -61,7 +136,7 @@ in {
     environment.systemPackages = [
       cfg.package
       cfg.package.run
-    ];
+    ] ++ lib.optional cfg.gamescopeSession.enable steam-gamescope;
 
     networking.firewall = lib.mkMerge [
       (mkIf cfg.remotePlay.openFirewall {
diff --git a/nixos/modules/programs/streamdeck-ui.nix b/nixos/modules/programs/streamdeck-ui.nix
index 113d1d49e15..4c055029e39 100644
--- a/nixos/modules/programs/streamdeck-ui.nix
+++ b/nixos/modules/programs/streamdeck-ui.nix
@@ -15,7 +15,7 @@ in
       description = lib.mdDoc "Whether streamdeck-ui should be started automatically.";
     };
 
-    package = mkPackageOption pkgs "streamdeck-ui" {
+    package = mkPackageOptionMD pkgs "streamdeck-ui" {
       default = [ "streamdeck-ui" ];
     };
 
diff --git a/nixos/modules/programs/tmux.nix b/nixos/modules/programs/tmux.nix
index 4fb9175fb8d..4f452f1d7f9 100644
--- a/nixos/modules/programs/tmux.nix
+++ b/nixos/modules/programs/tmux.nix
@@ -1,7 +1,7 @@
 { config, pkgs, lib, ... }:
 
 let
-  inherit (lib) mkOption mkIf types;
+  inherit (lib) mkOption mkIf types optionalString;
 
   cfg = config.programs.tmux;
 
@@ -17,17 +17,17 @@ let
     set  -g base-index      ${toString cfg.baseIndex}
     setw -g pane-base-index ${toString cfg.baseIndex}
 
-    ${if cfg.newSession then "new-session" else ""}
+    ${optionalString cfg.newSession "new-session"}
 
-    ${if cfg.reverseSplit then ''
+    ${optionalString cfg.reverseSplit ''
     bind v split-window -h
     bind s split-window -v
-    '' else ""}
+    ''}
 
     set -g status-keys ${cfg.keyMode}
     set -g mode-keys   ${cfg.keyMode}
 
-    ${if cfg.keyMode == "vi" && cfg.customPaneNavigationAndResize then ''
+    ${optionalString (cfg.keyMode == "vi" && cfg.customPaneNavigationAndResize) ''
     bind h select-pane -L
     bind j select-pane -D
     bind k select-pane -U
@@ -37,15 +37,15 @@ let
     bind -r J resize-pane -D ${toString cfg.resizeAmount}
     bind -r K resize-pane -U ${toString cfg.resizeAmount}
     bind -r L resize-pane -R ${toString cfg.resizeAmount}
-    '' else ""}
+    ''}
 
-    ${if (cfg.shortcut != defaultShortcut) then ''
+    ${optionalString (cfg.shortcut != defaultShortcut) ''
     # rebind main key: C-${cfg.shortcut}
     unbind C-${defaultShortcut}
     set -g prefix C-${cfg.shortcut}
     bind ${cfg.shortcut} send-prefix
     bind C-${cfg.shortcut} last-window
-    '' else ""}
+    ''}
 
     setw -g aggressive-resize ${boolToStr cfg.aggressiveResize}
     setw -g clock-mode-style  ${if cfg.clock24 then "24" else "12"}
@@ -160,7 +160,10 @@ in {
         default = defaultTerminal;
         example = "screen-256color";
         type = types.str;
-        description = lib.mdDoc "Set the $TERM variable.";
+        description = lib.mdDoc ''
+          Set the $TERM variable. Use tmux-direct if italics or 24bit true color
+          support is needed.
+        '';
       };
 
       secureSocket = mkOption {
diff --git a/nixos/modules/programs/trippy.nix b/nixos/modules/programs/trippy.nix
new file mode 100644
index 00000000000..6e31aea43e7
--- /dev/null
+++ b/nixos/modules/programs/trippy.nix
@@ -0,0 +1,24 @@
+{ lib, config, pkgs, ... }:
+
+let
+  cfg = config.programs.trippy;
+in
+
+{
+  options = {
+    programs.trippy = {
+      enable = lib.mkEnableOption (lib.mdDoc "trippy");
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    security.wrappers.trip = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_raw+p";
+      source = lib.getExe pkgs.trippy;
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ figsoda ];
+}
diff --git a/nixos/modules/programs/tsm-client.nix b/nixos/modules/programs/tsm-client.nix
index 7adff7cd28c..41560544c2c 100644
--- a/nixos/modules/programs/tsm-client.nix
+++ b/nixos/modules/programs/tsm-client.nix
@@ -6,7 +6,7 @@ let
   inherit (lib.attrsets) attrNames filterAttrs hasAttr mapAttrs mapAttrsToList optionalAttrs;
   inherit (lib.modules) mkDefault mkIf;
   inherit (lib.options) literalExpression mkEnableOption mkOption;
-  inherit (lib.strings) concatStringsSep optionalString toLower;
+  inherit (lib.strings) concatLines optionalString toLower;
   inherit (lib.types) addCheck attrsOf lines nonEmptyStr nullOr package path port str strMatching submodule;
 
   # Checks if given list of strings contains unique
@@ -164,7 +164,7 @@ let
         mkLine = k: v: k + optionalString (v!="") "  ${v}";
         lines = mapAttrsToList mkLine attrset;
       in
-        concatStringsSep "\n" lines;
+        concatLines lines;
     config.stanza = ''
       server  ${config.name}
       ${config.text}
@@ -263,7 +263,7 @@ let
 
     ${optionalString (cfg.defaultServername!=null) "defaultserver  ${cfg.defaultServername}"}
 
-    ${concatStringsSep "\n" (mapAttrsToList (k: v: v.stanza) cfg.servers)}
+    ${concatLines (mapAttrsToList (k: v: v.stanza) cfg.servers)}
   '';
 
 in
diff --git a/nixos/modules/programs/turbovnc.nix b/nixos/modules/programs/turbovnc.nix
index a0e4a36cfd9..511b6badc04 100644
--- a/nixos/modules/programs/turbovnc.nix
+++ b/nixos/modules/programs/turbovnc.nix
@@ -39,7 +39,7 @@ in
   config = mkIf cfg.ensureHeadlessSoftwareOpenGL {
 
     # TurboVNC has builtin support for Mesa llvmpipe's `swrast`
-    # software rendering to implemnt GLX (OpenGL on Xorg).
+    # software rendering to implement GLX (OpenGL on Xorg).
     # However, just building TurboVNC with support for that is not enough
     # (it only takes care of the X server side part of OpenGL);
     # the indiviudual applications (e.g. `glxgears`) also need to directly load
diff --git a/nixos/modules/programs/wayland/river.nix b/nixos/modules/programs/wayland/river.nix
new file mode 100644
index 00000000000..71232a7d261
--- /dev/null
+++ b/nixos/modules/programs/wayland/river.nix
@@ -0,0 +1,59 @@
+{
+  config,
+  pkgs,
+  lib,
+  ...
+}:
+with lib; let
+  cfg = config.programs.river;
+in {
+  options.programs.river = {
+    enable = mkEnableOption (lib.mdDoc "river, a dynamic tiling Wayland compositor");
+
+    package = mkOption {
+      type = with types; nullOr package;
+      default = pkgs.river;
+      defaultText = literalExpression "pkgs.river";
+      description = lib.mdDoc ''
+        River package to use.
+        Set to `null` to not add any River package to your path.
+        This should be done if you want to use the Home Manager River module to install River.
+      '';
+    };
+
+    extraPackages = mkOption {
+      type = with types; listOf package;
+      default = with pkgs; [
+        swaylock
+        foot
+        dmenu
+      ];
+      defaultText = literalExpression ''
+        with pkgs; [ swaylock foot dmenu ];
+      '';
+      example = literalExpression ''
+        with pkgs; [
+          termite rofi light
+        ]
+      '';
+      description = lib.mdDoc ''
+        Extra packages to be installed system wide. See
+        [Common X11 apps used on i3 with Wayland alternatives](https://github.com/swaywm/sway/wiki/i3-Migration-Guide#common-x11-apps-used-on-i3-with-wayland-alternatives)
+        for a list of useful software.
+      '';
+    };
+  };
+
+  config =
+    mkIf cfg.enable (mkMerge [
+      {
+        environment.systemPackages = optional (cfg.package != null) cfg.package ++ cfg.extraPackages;
+
+        # To make a river session available if a display manager like SDDM is enabled:
+        services.xserver.displayManager.sessionPackages = optionals (cfg.package != null) [ cfg.package ];
+      }
+      (import ./wayland-session.nix { inherit lib pkgs; })
+    ]);
+
+  meta.maintainers = with lib.maintainers; [ GaetanLepage ];
+}
diff --git a/nixos/modules/programs/sway.nix b/nixos/modules/programs/wayland/sway.nix
index b0a766dd055..698d9c2b46c 100644
--- a/nixos/modules/programs/sway.nix
+++ b/nixos/modules/programs/wayland/sway.nix
@@ -26,7 +26,7 @@ let
     };
   };
 
-  swayPackage = pkgs.sway.override {
+  defaultSwayPackage = pkgs.sway.override {
     extraSessionCommands = cfg.extraSessionCommands;
     extraOptions = cfg.extraOptions;
     withBaseWrapper = cfg.wrapperFeatures.base;
@@ -42,6 +42,19 @@ in {
       <https://github.com/swaywm/sway/wiki> and
       "man 5 sway" for more information'');
 
+    package = mkOption {
+      type = with types; nullOr package;
+      default = defaultSwayPackage;
+      defaultText = literalExpression "pkgs.sway";
+      description = lib.mdDoc ''
+        Sway package to use. Will override the options
+        'wrapperFeatures', 'extraSessionCommands', and 'extraOptions'.
+        Set to `null` to not add any Sway package to your
+        path. This should be done if you want to use the Home Manager Sway
+        module to install Sway.
+      '';
+    };
+
     wrapperFeatures = mkOption {
       type = wrapperOptions;
       default = { };
@@ -110,40 +123,36 @@ in {
 
   };
 
-  config = mkIf cfg.enable {
-    assertions = [
+  config = mkIf cfg.enable
+    (mkMerge [
       {
-        assertion = cfg.extraSessionCommands != "" -> cfg.wrapperFeatures.base;
-        message = ''
-          The extraSessionCommands for Sway will not be run if
-          wrapperFeatures.base is disabled.
-        '';
-      }
-    ];
-    environment = {
-      systemPackages = [ swayPackage ] ++ cfg.extraPackages;
-      # Needed for the default wallpaper:
-      pathsToLink = [ "/share/backgrounds/sway" ];
-      etc = {
-        "sway/config".source = mkOptionDefault "${swayPackage}/etc/sway/config";
-        "sway/config.d/nixos.conf".source = pkgs.writeText "nixos.conf" ''
-          # Import the most important environment variables into the D-Bus and systemd
-          # user environments (e.g. required for screen sharing and Pinentry prompts):
-          exec dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP
-        '';
-      };
-    };
-    security.polkit.enable = true;
-    security.pam.services.swaylock = {};
-    hardware.opengl.enable = mkDefault true;
-    fonts.enableDefaultFonts = mkDefault true;
-    programs.dconf.enable = mkDefault true;
-    # To make a Sway session available if a display manager like SDDM is enabled:
-    services.xserver.displayManager.sessionPackages = [ swayPackage ];
-    programs.xwayland.enable = mkDefault true;
-    # For screen sharing (this option only has an effect with xdg.portal.enable):
-    xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-wlr ];
-  };
+        assertions = [
+          {
+            assertion = cfg.extraSessionCommands != "" -> cfg.wrapperFeatures.base;
+            message = ''
+              The extraSessionCommands for Sway will not be run if
+              wrapperFeatures.base is disabled.
+            '';
+          }
+        ];
+        environment = {
+          systemPackages = optional (cfg.package != null) cfg.package ++ cfg.extraPackages;
+          # Needed for the default wallpaper:
+          pathsToLink = optionals (cfg.package != null) [ "/share/backgrounds/sway" ];
+          etc = {
+            "sway/config.d/nixos.conf".source = pkgs.writeText "nixos.conf" ''
+              # Import the most important environment variables into the D-Bus and systemd
+              # user environments (e.g. required for screen sharing and Pinentry prompts):
+              exec dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP
+            '';
+          } // optionalAttrs (cfg.package != null) {
+            "sway/config".source = mkOptionDefault "${cfg.package}/etc/sway/config";
+          };
+        };
+        # To make a Sway session available if a display manager like SDDM is enabled:
+        services.xserver.displayManager.sessionPackages = optionals (cfg.package != null) [ cfg.package ]; }
+      (import ./wayland-session.nix { inherit lib pkgs; })
+    ]);
 
   meta.maintainers = with lib.maintainers; [ primeos colemickens ];
 }
diff --git a/nixos/modules/programs/waybar.nix b/nixos/modules/programs/wayland/waybar.nix
index 4697d0f7a62..2c49ae14081 100644
--- a/nixos/modules/programs/waybar.nix
+++ b/nixos/modules/programs/wayland/waybar.nix
@@ -2,17 +2,22 @@
 
 with lib;
 
+let
+  cfg = config.programs.waybar;
+in
 {
   options.programs.waybar = {
     enable = mkEnableOption (lib.mdDoc "waybar");
+    package = mkPackageOptionMD pkgs "waybar" { };
   };
 
-  config = mkIf config.programs.waybar.enable {
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
     systemd.user.services.waybar = {
       description = "Waybar as systemd service";
       wantedBy = [ "graphical-session.target" ];
       partOf = [ "graphical-session.target" ];
-      script = "${pkgs.waybar}/bin/waybar";
+      script = "${cfg.package}/bin/waybar";
     };
   };
 
diff --git a/nixos/modules/programs/wayland/wayland-session.nix b/nixos/modules/programs/wayland/wayland-session.nix
new file mode 100644
index 00000000000..3cbfef4d61d
--- /dev/null
+++ b/nixos/modules/programs/wayland/wayland-session.nix
@@ -0,0 +1,23 @@
+{ lib, pkgs, ... }: with lib; {
+    security = {
+      polkit.enable = true;
+      pam.services.swaylock = {};
+    };
+
+    hardware.opengl.enable = mkDefault true;
+    fonts.enableDefaultFonts = mkDefault true;
+
+    programs = {
+      dconf.enable = mkDefault true;
+      xwayland.enable = mkDefault true;
+    };
+
+    xdg.portal = {
+      enable = mkDefault true;
+
+      extraPortals = [
+        # For screen sharing
+        pkgs.xdg-desktop-portal-wlr
+      ];
+    };
+}
diff --git a/nixos/modules/programs/wireshark.nix b/nixos/modules/programs/wireshark.nix
index 088c2bb7958..834b0ba3569 100644
--- a/nixos/modules/programs/wireshark.nix
+++ b/nixos/modules/programs/wireshark.nix
@@ -33,7 +33,7 @@ in {
 
     security.wrappers.dumpcap = {
       source = "${wireshark}/bin/dumpcap";
-      capabilities = "cap_net_raw+p";
+      capabilities = "cap_net_raw,cap_net_admin+eip";
       owner = "root";
       group = "wireshark";
       permissions = "u+rx,g+x";
diff --git a/nixos/modules/programs/xastir.nix b/nixos/modules/programs/xastir.nix
index 0977668d837..6d5fc59aac5 100644
--- a/nixos/modules/programs/xastir.nix
+++ b/nixos/modules/programs/xastir.nix
@@ -8,7 +8,7 @@ in {
   meta.maintainers = with maintainers; [ melling ];
 
   options.programs.xastir = {
-    enable = mkEnableOption (mdDoc "Enable Xastir Graphical APRS client");
+    enable = mkEnableOption (mdDoc "Xastir Graphical APRS client");
   };
 
   config = mkIf cfg.enable {
diff --git a/nixos/modules/programs/xonsh.nix b/nixos/modules/programs/xonsh.nix
index 7202ed06c6a..167c953f5ff 100644
--- a/nixos/modules/programs/xonsh.nix
+++ b/nixos/modules/programs/xonsh.nix
@@ -28,7 +28,7 @@ in
         type = types.package;
         default = pkgs.xonsh;
         defaultText = literalExpression "pkgs.xonsh";
-        example = literalExpression "pkgs.xonsh.override { configFile = \"/path/to/xonshrc\"; }";
+        example = literalExpression "pkgs.xonsh.override { extraPackages = ps: [ ps.requests ]; }";
         description = lib.mdDoc ''
           xonsh package to use.
         '';
@@ -83,4 +83,3 @@ in
   };
 
 }
-
diff --git a/nixos/modules/programs/zsh/oh-my-zsh.md b/nixos/modules/programs/zsh/oh-my-zsh.md
new file mode 100644
index 00000000000..73d425244ce
--- /dev/null
+++ b/nixos/modules/programs/zsh/oh-my-zsh.md
@@ -0,0 +1,109 @@
+# Oh my ZSH {#module-programs-zsh-ohmyzsh}
+
+[`oh-my-zsh`](https://ohmyz.sh/) is a framework to manage your [ZSH](https://www.zsh.org/)
+configuration including completion scripts for several CLI tools or custom
+prompt themes.
+
+## Basic usage {#module-programs-oh-my-zsh-usage}
+
+The module uses the `oh-my-zsh` package with all available
+features. The initial setup using Nix expressions is fairly similar to the
+configuration format of `oh-my-zsh`.
+```
+{
+  programs.zsh.ohMyZsh = {
+    enable = true;
+    plugins = [ "git" "python" "man" ];
+    theme = "agnoster";
+  };
+}
+```
+For a detailed explanation of these arguments please refer to the
+[`oh-my-zsh` docs](https://github.com/robbyrussell/oh-my-zsh/wiki).
+
+The expression generates the needed configuration and writes it into your
+`/etc/zshrc`.
+
+## Custom additions {#module-programs-oh-my-zsh-additions}
+
+Sometimes third-party or custom scripts such as a modified theme may be
+needed. `oh-my-zsh` provides the
+[`ZSH_CUSTOM`](https://github.com/robbyrussell/oh-my-zsh/wiki/Customization#overriding-internals)
+environment variable for this which points to a directory with additional
+scripts.
+
+The module can do this as well:
+```
+{
+  programs.zsh.ohMyZsh.custom = "~/path/to/custom/scripts";
+}
+```
+
+## Custom environments {#module-programs-oh-my-zsh-environments}
+
+There are several extensions for `oh-my-zsh` packaged in
+`nixpkgs`. One of them is
+[nix-zsh-completions](https://github.com/spwhitt/nix-zsh-completions)
+which bundles completion scripts and a plugin for `oh-my-zsh`.
+
+Rather than using a single mutable path for `ZSH_CUSTOM`,
+it's also possible to generate this path from a list of Nix packages:
+```
+{ pkgs, ... }:
+{
+  programs.zsh.ohMyZsh.customPkgs = [
+    pkgs.nix-zsh-completions
+    # and even more...
+  ];
+}
+```
+Internally a single store path will be created using
+`buildEnv`. Please refer to the docs of
+[`buildEnv`](https://nixos.org/nixpkgs/manual/#sec-building-environment)
+for further reference.
+
+*Please keep in mind that this is not compatible with
+`programs.zsh.ohMyZsh.custom` as it requires an immutable
+store path while `custom` shall remain mutable! An
+evaluation failure will be thrown if both `custom` and
+`customPkgs` are set.*
+
+## Package your own customizations {#module-programs-oh-my-zsh-packaging-customizations}
+
+If third-party customizations (e.g. new themes) are supposed to be added to
+`oh-my-zsh` there are several pitfalls to keep in mind:
+
+  - To comply with the default structure of `ZSH` the entire
+    output needs to be written to `$out/share/zsh.`
+
+  - Completion scripts are supposed to be stored at
+    `$out/share/zsh/site-functions`. This directory is part of the
+    [`fpath`](http://zsh.sourceforge.net/Doc/Release/Functions.html)
+    and the package should be compatible with pure `ZSH`
+    setups. The module will automatically link the contents of
+    `site-functions` to completions directory in the proper
+    store path.
+
+  - The `plugins` directory needs the structure
+    `pluginname/pluginname.plugin.zsh` as structured in the
+    [upstream repo.](https://github.com/robbyrussell/oh-my-zsh/tree/91b771914bc7c43dd7c7a43b586c5de2c225ceb7/plugins)
+
+A derivation for `oh-my-zsh` may look like this:
+```
+{ stdenv, fetchFromGitHub }:
+
+stdenv.mkDerivation rec {
+  name = "exemplary-zsh-customization-${version}";
+  version = "1.0.0";
+  src = fetchFromGitHub {
+    # path to the upstream repository
+  };
+
+  dontBuild = true;
+  installPhase = ''
+    mkdir -p $out/share/zsh/site-functions
+    cp {themes,plugins} $out/share/zsh
+    cp completions $out/share/zsh/site-functions
+  '';
+}
+```
diff --git a/nixos/modules/programs/zsh/oh-my-zsh.nix b/nixos/modules/programs/zsh/oh-my-zsh.nix
index 41ea31b0f12..83eee1c88b3 100644
--- a/nixos/modules/programs/zsh/oh-my-zsh.nix
+++ b/nixos/modules/programs/zsh/oh-my-zsh.nix
@@ -142,5 +142,5 @@ in
 
     };
 
-    meta.doc = ./oh-my-zsh.xml;
+    meta.doc = ./oh-my-zsh.md;
   }
diff --git a/nixos/modules/programs/zsh/oh-my-zsh.xml b/nixos/modules/programs/zsh/oh-my-zsh.xml
deleted file mode 100644
index 14a7228ad9b..00000000000
--- a/nixos/modules/programs/zsh/oh-my-zsh.xml
+++ /dev/null
@@ -1,155 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-programs-zsh-ohmyzsh">
- <title>Oh my ZSH</title>
- <para>
-  <literal><link xlink:href="https://ohmyz.sh/">oh-my-zsh</link></literal> is a
-  framework to manage your <link xlink:href="https://www.zsh.org/">ZSH</link>
-  configuration including completion scripts for several CLI tools or custom
-  prompt themes.
- </para>
- <section xml:id="module-programs-oh-my-zsh-usage">
-  <title>Basic usage</title>
-
-  <para>
-   The module uses the <literal>oh-my-zsh</literal> package with all available
-   features. The initial setup using Nix expressions is fairly similar to the
-   configuration format of <literal>oh-my-zsh</literal>.
-<programlisting>
-{
-  programs.zsh.ohMyZsh = {
-    enable = true;
-    plugins = [ "git" "python" "man" ];
-    theme = "agnoster";
-  };
-}
-</programlisting>
-   For a detailed explanation of these arguments please refer to the
-   <link xlink:href="https://github.com/robbyrussell/oh-my-zsh/wiki"><literal>oh-my-zsh</literal>
-   docs</link>.
-  </para>
-
-  <para>
-   The expression generates the needed configuration and writes it into your
-   <literal>/etc/zshrc</literal>.
-  </para>
- </section>
- <section xml:id="module-programs-oh-my-zsh-additions">
-  <title>Custom additions</title>
-
-  <para>
-   Sometimes third-party or custom scripts such as a modified theme may be
-   needed. <literal>oh-my-zsh</literal> provides the
-   <link xlink:href="https://github.com/robbyrussell/oh-my-zsh/wiki/Customization#overriding-internals"><literal>ZSH_CUSTOM</literal></link>
-   environment variable for this which points to a directory with additional
-   scripts.
-  </para>
-
-  <para>
-   The module can do this as well:
-<programlisting>
-{
-  programs.zsh.ohMyZsh.custom = "~/path/to/custom/scripts";
-}
-</programlisting>
-  </para>
- </section>
- <section xml:id="module-programs-oh-my-zsh-environments">
-  <title>Custom environments</title>
-
-  <para>
-   There are several extensions for <literal>oh-my-zsh</literal> packaged in
-   <literal>nixpkgs</literal>. One of them is
-   <link xlink:href="https://github.com/spwhitt/nix-zsh-completions">nix-zsh-completions</link>
-   which bundles completion scripts and a plugin for
-   <literal>oh-my-zsh</literal>.
-  </para>
-
-  <para>
-   Rather than using a single mutable path for <literal>ZSH_CUSTOM</literal>,
-   it's also possible to generate this path from a list of Nix packages:
-<programlisting>
-{ pkgs, ... }:
-{
-  programs.zsh.ohMyZsh.customPkgs = [
-    pkgs.nix-zsh-completions
-    # and even more...
-  ];
-}
-</programlisting>
-   Internally a single store path will be created using
-   <literal>buildEnv</literal>. Please refer to the docs of
-   <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-building-environment"><literal>buildEnv</literal></link>
-   for further reference.
-  </para>
-
-  <para>
-   <emphasis>Please keep in mind that this is not compatible with
-   <literal>programs.zsh.ohMyZsh.custom</literal> as it requires an immutable
-   store path while <literal>custom</literal> shall remain mutable! An
-   evaluation failure will be thrown if both <literal>custom</literal> and
-   <literal>customPkgs</literal> are set.</emphasis>
-  </para>
- </section>
- <section xml:id="module-programs-oh-my-zsh-packaging-customizations">
-  <title>Package your own customizations</title>
-
-  <para>
-   If third-party customizations (e.g. new themes) are supposed to be added to
-   <literal>oh-my-zsh</literal> there are several pitfalls to keep in mind:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     To comply with the default structure of <literal>ZSH</literal> the entire
-     output needs to be written to <literal>$out/share/zsh.</literal>
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Completion scripts are supposed to be stored at
-     <literal>$out/share/zsh/site-functions</literal>. This directory is part
-     of the
-     <literal><link xlink:href="http://zsh.sourceforge.net/Doc/Release/Functions.html">fpath</link></literal>
-     and the package should be compatible with pure <literal>ZSH</literal>
-     setups. The module will automatically link the contents of
-     <literal>site-functions</literal> to completions directory in the proper
-     store path.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     The <literal>plugins</literal> directory needs the structure
-     <literal>pluginname/pluginname.plugin.zsh</literal> as structured in the
-     <link xlink:href="https://github.com/robbyrussell/oh-my-zsh/tree/91b771914bc7c43dd7c7a43b586c5de2c225ceb7/plugins">upstream
-     repo.</link>
-    </para>
-   </listitem>
-  </itemizedlist>
-
-  <para>
-   A derivation for <literal>oh-my-zsh</literal> may look like this:
-<programlisting>
-{ stdenv, fetchFromGitHub }:
-
-stdenv.mkDerivation rec {
-  name = "exemplary-zsh-customization-${version}";
-  version = "1.0.0";
-  src = fetchFromGitHub {
-    # path to the upstream repository
-  };
-
-  dontBuild = true;
-  installPhase = ''
-    mkdir -p $out/share/zsh/site-functions
-    cp {themes,plugins} $out/share/zsh
-    cp completions $out/share/zsh/site-functions
-  '';
-}
-</programlisting>
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix b/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
index bc110134928..cec4be1cb01 100644
--- a/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
+++ b/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
@@ -26,6 +26,7 @@ in
           "brackets"
           "pattern"
           "cursor"
+          "regexp"
           "root"
           "line"
         ]));
diff --git a/nixos/modules/programs/zsh/zsh.nix b/nixos/modules/programs/zsh/zsh.nix
index 0b152e54cf9..6bb21cb3ef6 100644
--- a/nixos/modules/programs/zsh/zsh.nix
+++ b/nixos/modules/programs/zsh/zsh.nix
@@ -236,6 +236,9 @@ in
           setopt ${concatStringsSep " " cfg.setOptions}
         ''}
 
+        # Alternative method of determining short and full hostname.
+        HOST=${config.networking.fqdnOrHostName}
+
         # Setup command line history.
         # Don't export these, otherwise other shells (bash) will try to use same HISTFILE.
         SAVEHIST=${toString cfg.histSize}
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index aef42d0f4db..0e8b823c2bd 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -1,7 +1,10 @@
 { lib, pkgs, ... }:
 
-with lib;
-
+let
+  inherit (lib)
+    mkAliasOptionModuleMD
+    mkRemovedOptionModule;
+in
 {
   imports = [
     /*
@@ -14,7 +17,7 @@ with lib;
 
     # This alias module can't be where _module.check is defined because it would
     # be added to submodules as well there
-    (mkAliasOptionModule [ "environment" "checkConfigurationOptions" ] [ "_module" "check" ])
+    (mkAliasOptionModuleMD [ "environment" "checkConfigurationOptions" ] [ "_module" "check" ])
 
     # Completely removed modules
     (mkRemovedOptionModule [ "environment" "blcr" "enable" ] "The BLCR module has been removed")
@@ -36,6 +39,7 @@ with lib;
     '')
     (mkRemovedOptionModule [ "networking" "vpnc" ] "Use environment.etc.\"vpnc/service.conf\" instead.")
     (mkRemovedOptionModule [ "networking" "wicd" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "programs" "gnome-documents" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "programs" "tilp2" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "programs" "way-cooler" ] ("way-cooler is abandoned by its author: " +
       "https://way-cooler.org/blog/2020/01/09/way-cooler-post-mortem.html"))
@@ -43,20 +47,23 @@ with lib;
         The hidepid module was removed, since the underlying machinery
         is broken when using cgroups-v2.
     '')
+    (mkRemovedOptionModule [ "services" "baget" "enable" ] "The baget module was removed due to the upstream package being unmaintained.")
     (mkRemovedOptionModule [ "services" "beegfs" ] "The BeeGFS module has been removed")
     (mkRemovedOptionModule [ "services" "beegfsEnable" ] "The BeeGFS module has been removed")
     (mkRemovedOptionModule [ "services" "cgmanager" "enable"] "cgmanager was deprecated by lxc and therefore removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "chronos" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "couchpotato" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "dd-agent" ] "dd-agent was removed from nixpkgs in favor of the newer datadog-agent.")
-    (mkRemovedOptionModule [ "services" "deepin" ] "The corresponding packages were removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "ddclient" ] "ddclient has been removed on the request of the upstream maintainer because it is unmaintained and has bugs. Please switch to a different software like `inadyn` or `knsupdate`.") # Added 2023-07-04
     (mkRemovedOptionModule [ "services" "dnscrypt-proxy" ] "Use services.dnscrypt-proxy2 instead")
+    (mkRemovedOptionModule [ "services" "exhibitor" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "firefox" "syncserver" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "flashpolicyd" ] "The flashpolicyd module has been removed. Adobe Flash Player is deprecated.")
     (mkRemovedOptionModule [ "services" "fourStore" ] "The fourStore module has been removed")
     (mkRemovedOptionModule [ "services" "fourStoreEndpoint" ] "The fourStoreEndpoint module has been removed")
     (mkRemovedOptionModule [ "services" "fprot" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "frab" ] "The frab module has been removed")
+    (mkRemovedOptionModule [ "services" "ihatemoney" ] "The ihatemoney module has been removed for lack of downstream maintainer")
     (mkRemovedOptionModule [ "services" "kippo" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "mailpile" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "marathon" ] "The corresponding package was removed from nixpkgs.")
@@ -105,6 +112,9 @@ with lib;
     (mkRemovedOptionModule [ "services" "openfire" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "riak" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "cryptpad" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "services" "rtsp-simple-server" ] "Package has been completely rebranded by upstream as mediamtx, and thus the service and the package were renamed in NixOS as well.")
+
+    (mkRemovedOptionModule [ "i18n" "inputMethod" "fcitx" ] "The fcitx module has been removed. Please use fcitx5 instead")
 
     # Do NOT add any option renames here, see top of the file
   ];
diff --git a/nixos/modules/security/acme/default.md b/nixos/modules/security/acme/default.md
new file mode 100644
index 00000000000..8ff97b55f68
--- /dev/null
+++ b/nixos/modules/security/acme/default.md
@@ -0,0 +1,354 @@
+# SSL/TLS Certificates with ACME {#module-security-acme}
+
+NixOS supports automatic domain validation & certificate retrieval and
+renewal using the ACME protocol. Any provider can be used, but by default
+NixOS uses Let's Encrypt. The alternative ACME client
+[lego](https://go-acme.github.io/lego/) is used under
+the hood.
+
+Automatic cert validation and configuration for Apache and Nginx virtual
+hosts is included in NixOS, however if you would like to generate a wildcard
+cert or you are not using a web server you will have to configure DNS
+based validation.
+
+## Prerequisites {#module-security-acme-prerequisites}
+
+To use the ACME module, you must accept the provider's terms of service
+by setting [](#opt-security.acme.acceptTerms)
+to `true`. The Let's Encrypt ToS can be found
+[here](https://letsencrypt.org/repository/).
+
+You must also set an email address to be used when creating accounts with
+Let's Encrypt. You can set this for all certs with
+[](#opt-security.acme.defaults.email)
+and/or on a per-cert basis with
+[](#opt-security.acme.certs._name_.email).
+This address is only used for registration and renewal reminders,
+and cannot be used to administer the certificates in any way.
+
+Alternatively, you can use a different ACME server by changing the
+[](#opt-security.acme.defaults.server) option
+to a provider of your choosing, or just change the server for one cert with
+[](#opt-security.acme.certs._name_.server).
+
+You will need an HTTP server or DNS server for verification. For HTTP,
+the server must have a webroot defined that can serve
+{file}`.well-known/acme-challenge`. This directory must be
+writeable by the user that will run the ACME client. For DNS, you must
+set up credentials with your provider/server for use with lego.
+
+## Using ACME certificates in Nginx {#module-security-acme-nginx}
+
+NixOS supports fetching ACME certificates for you by setting
+`enableACME = true;` in a virtualHost config. We first create self-signed
+placeholder certificates in place of the real ACME certs. The placeholder
+certs are overwritten when the ACME certs arrive. For
+`foo.example.com` the config would look like this:
+
+```
+security.acme.acceptTerms = true;
+security.acme.defaults.email = "admin+acme@example.com";
+services.nginx = {
+  enable = true;
+  virtualHosts = {
+    "foo.example.com" = {
+      forceSSL = true;
+      enableACME = true;
+      # All serverAliases will be added as extra domain names on the certificate.
+      serverAliases = [ "bar.example.com" ];
+      locations."/" = {
+        root = "/var/www";
+      };
+    };
+
+    # We can also add a different vhost and reuse the same certificate
+    # but we have to append extraDomainNames manually beforehand:
+    # security.acme.certs."foo.example.com".extraDomainNames = [ "baz.example.com" ];
+    "baz.example.com" = {
+      forceSSL = true;
+      useACMEHost = "foo.example.com";
+      locations."/" = {
+        root = "/var/www";
+      };
+    };
+  };
+}
+```
+
+## Using ACME certificates in Apache/httpd {#module-security-acme-httpd}
+
+Using ACME certificates with Apache virtual hosts is identical
+to using them with Nginx. The attribute names are all the same, just replace
+"nginx" with "httpd" where appropriate.
+
+## Manual configuration of HTTP-01 validation {#module-security-acme-configuring}
+
+First off you will need to set up a virtual host to serve the challenges.
+This example uses a vhost called `certs.example.com`, with
+the intent that you will generate certs for all your vhosts and redirect
+everyone to HTTPS.
+
+```
+security.acme.acceptTerms = true;
+security.acme.defaults.email = "admin+acme@example.com";
+
+# /var/lib/acme/.challenges must be writable by the ACME user
+# and readable by the Nginx user. The easiest way to achieve
+# this is to add the Nginx user to the ACME group.
+users.users.nginx.extraGroups = [ "acme" ];
+
+services.nginx = {
+  enable = true;
+  virtualHosts = {
+    "acmechallenge.example.com" = {
+      # Catchall vhost, will redirect users to HTTPS for all vhosts
+      serverAliases = [ "*.example.com" ];
+      locations."/.well-known/acme-challenge" = {
+        root = "/var/lib/acme/.challenges";
+      };
+      locations."/" = {
+        return = "301 https://$host$request_uri";
+      };
+    };
+  };
+}
+# Alternative config for Apache
+users.users.wwwrun.extraGroups = [ "acme" ];
+services.httpd = {
+  enable = true;
+  virtualHosts = {
+    "acmechallenge.example.com" = {
+      # Catchall vhost, will redirect users to HTTPS for all vhosts
+      serverAliases = [ "*.example.com" ];
+      # /var/lib/acme/.challenges must be writable by the ACME user and readable by the Apache user.
+      # By default, this is the case.
+      documentRoot = "/var/lib/acme/.challenges";
+      extraConfig = ''
+        RewriteEngine On
+        RewriteCond %{HTTPS} off
+        RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge [NC]
+        RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301]
+      '';
+    };
+  };
+}
+```
+
+Now you need to configure ACME to generate a certificate.
+
+```
+security.acme.certs."foo.example.com" = {
+  webroot = "/var/lib/acme/.challenges";
+  email = "foo@example.com";
+  # Ensure that the web server you use can read the generated certs
+  # Take a look at the group option for the web server you choose.
+  group = "nginx";
+  # Since we have a wildcard vhost to handle port 80,
+  # we can generate certs for anything!
+  # Just make sure your DNS resolves them.
+  extraDomainNames = [ "mail.example.com" ];
+};
+```
+
+The private key {file}`key.pem` and certificate
+{file}`fullchain.pem` will be put into
+{file}`/var/lib/acme/foo.example.com`.
+
+Refer to [](#ch-options) for all available configuration
+options for the [security.acme](#opt-security.acme.certs)
+module.
+
+## Configuring ACME for DNS validation {#module-security-acme-config-dns}
+
+This is useful if you want to generate a wildcard certificate, since
+ACME servers will only hand out wildcard certs over DNS validation.
+There are a number of supported DNS providers and servers you can utilise,
+see the [lego docs](https://go-acme.github.io/lego/dns/)
+for provider/server specific configuration values. For the sake of these
+docs, we will provide a fully self-hosted example using bind.
+
+```
+services.bind = {
+  enable = true;
+  extraConfig = ''
+    include "/var/lib/secrets/dnskeys.conf";
+  '';
+  zones = [
+    rec {
+      name = "example.com";
+      file = "/var/db/bind/${name}";
+      master = true;
+      extraConfig = "allow-update { key rfc2136key.example.com.; };";
+    }
+  ];
+}
+
+# Now we can configure ACME
+security.acme.acceptTerms = true;
+security.acme.defaults.email = "admin+acme@example.com";
+security.acme.certs."example.com" = {
+  domain = "*.example.com";
+  dnsProvider = "rfc2136";
+  credentialsFile = "/var/lib/secrets/certs.secret";
+  # We don't need to wait for propagation since this is a local DNS server
+  dnsPropagationCheck = false;
+};
+```
+
+The {file}`dnskeys.conf` and {file}`certs.secret`
+must be kept secure and thus you should not keep their contents in your
+Nix config. Instead, generate them one time with a systemd service:
+
+```
+systemd.services.dns-rfc2136-conf = {
+  requiredBy = ["acme-example.com.service" "bind.service"];
+  before = ["acme-example.com.service" "bind.service"];
+  unitConfig = {
+    ConditionPathExists = "!/var/lib/secrets/dnskeys.conf";
+  };
+  serviceConfig = {
+    Type = "oneshot";
+    UMask = 0077;
+  };
+  path = [ pkgs.bind ];
+  script = ''
+    mkdir -p /var/lib/secrets
+    chmod 755 /var/lib/secrets
+    tsig-keygen rfc2136key.example.com > /var/lib/secrets/dnskeys.conf
+    chown named:root /var/lib/secrets/dnskeys.conf
+    chmod 400 /var/lib/secrets/dnskeys.conf
+
+    # extract secret value from the dnskeys.conf
+    while read x y; do if [ "$x" = "secret" ]; then secret="''${y:1:''${#y}-3}"; fi; done < /var/lib/secrets/dnskeys.conf
+
+    cat > /var/lib/secrets/certs.secret << EOF
+    RFC2136_NAMESERVER='127.0.0.1:53'
+    RFC2136_TSIG_ALGORITHM='hmac-sha256.'
+    RFC2136_TSIG_KEY='rfc2136key.example.com'
+    RFC2136_TSIG_SECRET='$secret'
+    EOF
+    chmod 400 /var/lib/secrets/certs.secret
+  '';
+};
+```
+
+Now you're all set to generate certs! You should monitor the first invocation
+by running `systemctl start acme-example.com.service &
+journalctl -fu acme-example.com.service` and watching its log output.
+
+## Using DNS validation with web server virtual hosts {#module-security-acme-config-dns-with-vhosts}
+
+It is possible to use DNS-01 validation with all certificates,
+including those automatically configured via the Nginx/Apache
+[`enableACME`](#opt-services.nginx.virtualHosts._name_.enableACME)
+option. This configuration pattern is fully
+supported and part of the module's test suite for Nginx + Apache.
+
+You must follow the guide above on configuring DNS-01 validation
+first, however instead of setting the options for one certificate
+(e.g. [](#opt-security.acme.certs._name_.dnsProvider))
+you will set them as defaults
+(e.g. [](#opt-security.acme.defaults.dnsProvider)).
+
+```
+# Configure ACME appropriately
+security.acme.acceptTerms = true;
+security.acme.defaults.email = "admin+acme@example.com";
+security.acme.defaults = {
+  dnsProvider = "rfc2136";
+  credentialsFile = "/var/lib/secrets/certs.secret";
+  # We don't need to wait for propagation since this is a local DNS server
+  dnsPropagationCheck = false;
+};
+
+# For each virtual host you would like to use DNS-01 validation with,
+# set acmeRoot = null
+services.nginx = {
+  enable = true;
+  virtualHosts = {
+    "foo.example.com" = {
+      enableACME = true;
+      acmeRoot = null;
+    };
+  };
+}
+```
+
+And that's it! Next time your configuration is rebuilt, or when
+you add a new virtualHost, it will be DNS-01 validated.
+
+## Using ACME with services demanding root owned certificates {#module-security-acme-root-owned}
+
+Some services refuse to start if the configured certificate files
+are not owned by root. PostgreSQL and OpenSMTPD are examples of these.
+There is no way to change the user the ACME module uses (it will always be
+`acme`), however you can use systemd's
+`LoadCredential` feature to resolve this elegantly.
+Below is an example configuration for OpenSMTPD, but this pattern
+can be applied to any service.
+
+```
+# Configure ACME however you like (DNS or HTTP validation), adding
+# the following configuration for the relevant certificate.
+# Note: You cannot use `systemctl reload` here as that would mean
+# the LoadCredential configuration below would be skipped and
+# the service would continue to use old certificates.
+security.acme.certs."mail.example.com".postRun = ''
+  systemctl restart opensmtpd
+'';
+
+# Now you must augment OpenSMTPD's systemd service to load
+# the certificate files.
+systemd.services.opensmtpd.requires = ["acme-finished-mail.example.com.target"];
+systemd.services.opensmtpd.serviceConfig.LoadCredential = let
+  certDir = config.security.acme.certs."mail.example.com".directory;
+in [
+  "cert.pem:${certDir}/cert.pem"
+  "key.pem:${certDir}/key.pem"
+];
+
+# Finally, configure OpenSMTPD to use these certs.
+services.opensmtpd = let
+  credsDir = "/run/credentials/opensmtpd.service";
+in {
+  enable = true;
+  setSendmail = false;
+  serverConfiguration = ''
+    pki mail.example.com cert "${credsDir}/cert.pem"
+    pki mail.example.com key "${credsDir}/key.pem"
+    listen on localhost tls pki mail.example.com
+    action act1 relay host smtp://127.0.0.1:10027
+    match for local action act1
+  '';
+};
+```
+
+## Regenerating certificates {#module-security-acme-regenerate}
+
+Should you need to regenerate a particular certificate in a hurry, such
+as when a vulnerability is found in Let's Encrypt, there is now a convenient
+mechanism for doing so. Running
+`systemctl clean --what=state acme-example.com.service`
+will remove all certificate files and the account data for the given domain,
+allowing you to then `systemctl start acme-example.com.service`
+to generate fresh ones.
+
+## Fixing JWS Verification error {#module-security-acme-fix-jws}
+
+It is possible that your account credentials file may become corrupt and need
+to be regenerated. In this scenario lego will produce the error `JWS verification error`.
+The solution is to simply delete the associated accounts file and
+re-run the affected service(s).
+
+```
+# Find the accounts folder for the certificate
+systemctl cat acme-example.com.service | grep -Po 'accounts/[^:]*'
+export accountdir="$(!!)"
+# Move this folder to some place else
+mv /var/lib/acme/.lego/$accountdir{,.bak}
+# Recreate the folder using systemd-tmpfiles
+systemd-tmpfiles --create
+# Get a new account and reissue certificates
+# Note: Do this for all certs that share the same account email address
+systemctl start acme-example.com.service
+```
diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix
index a380bb5484a..8aee71c864d 100644
--- a/nixos/modules/security/acme/default.nix
+++ b/nixos/modules/security/acme/default.nix
@@ -323,7 +323,7 @@ let
             }
           fi
         '');
-      } // optionalAttrs (data.listenHTTP != null && toInt (elemAt (splitString ":" data.listenHTTP) 1) < 1024) {
+      } // optionalAttrs (data.listenHTTP != null && toInt (last (splitString ":" data.listenHTTP)) < 1024) {
         CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
         AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
       };
@@ -487,7 +487,7 @@ let
       };
 
       email = mkOption {
-        type = types.str;
+        type = types.nullOr types.str;
         inherit (defaultAndText "email" null) default defaultText;
         description = lib.mdDoc ''
           Email address for account creation and correspondence from the CA.
@@ -555,7 +555,7 @@ let
       };
 
       credentialsFile = mkOption {
-        type = types.path;
+        type = types.nullOr types.path;
         inherit (defaultAndText "credentialsFile" null) default defaultText;
         description = lib.mdDoc ''
           Path to an EnvironmentFile for the cert's service containing any required and
@@ -727,7 +727,7 @@ in {
           Default values inheritable by all configured certs. You can
           use this to define options shared by all your certs. These defaults
           can also be ignored on a per-cert basis using the
-          `security.acme.certs.''${cert}.inheritDefaults' option.
+          {option}`security.acme.certs.''${cert}.inheritDefaults` option.
         '';
       };
 
@@ -781,11 +781,11 @@ in {
 
       # FIXME Most of these custom warnings and filters for security.acme.certs.* are required
       # because using mkRemovedOptionModule/mkChangedOptionModule with attrsets isn't possible.
-      warnings = filter (w: w != "") (mapAttrsToList (cert: data: if data.extraDomains != "_mkMergedOptionModule" then ''
+      warnings = filter (w: w != "") (mapAttrsToList (cert: data: optionalString (data.extraDomains != "_mkMergedOptionModule") ''
         The option definition `security.acme.certs.${cert}.extraDomains` has changed
         to `security.acme.certs.${cert}.extraDomainNames` and is now a list of strings.
         Setting a custom webroot for extra domains is not possible, instead use separate certs.
-      '' else "") cfg.certs);
+      '') cfg.certs);
 
       assertions = let
         certs = attrValues cfg.certs;
@@ -916,6 +916,6 @@ in {
 
   meta = {
     maintainers = lib.teams.acme.members;
-    doc = ./doc.xml;
+    doc = ./default.md;
   };
 }
diff --git a/nixos/modules/security/acme/doc.xml b/nixos/modules/security/acme/doc.xml
deleted file mode 100644
index 1439594a5ac..00000000000
--- a/nixos/modules/security/acme/doc.xml
+++ /dev/null
@@ -1,414 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-security-acme">
- <title>SSL/TLS Certificates with ACME</title>
- <para>
-  NixOS supports automatic domain validation &amp; certificate retrieval and
-  renewal using the ACME protocol. Any provider can be used, but by default
-  NixOS uses Let's Encrypt. The alternative ACME client
-  <link xlink:href="https://go-acme.github.io/lego/">lego</link> is used under
-  the hood.
- </para>
- <para>
-  Automatic cert validation and configuration for Apache and Nginx virtual
-  hosts is included in NixOS, however if you would like to generate a wildcard
-  cert or you are not using a web server you will have to configure DNS
-  based validation.
- </para>
- <section xml:id="module-security-acme-prerequisites">
-  <title>Prerequisites</title>
-
-  <para>
-   To use the ACME module, you must accept the provider's terms of service
-   by setting <literal><xref linkend="opt-security.acme.acceptTerms" /></literal>
-   to <literal>true</literal>. The Let's Encrypt ToS can be found
-   <link xlink:href="https://letsencrypt.org/repository/">here</link>.
-  </para>
-
-  <para>
-   You must also set an email address to be used when creating accounts with
-   Let's Encrypt. You can set this for all certs with
-   <literal><xref linkend="opt-security.acme.defaults.email" /></literal>
-   and/or on a per-cert basis with
-   <literal><xref linkend="opt-security.acme.certs._name_.email" /></literal>.
-   This address is only used for registration and renewal reminders,
-   and cannot be used to administer the certificates in any way.
-  </para>
-
-  <para>
-   Alternatively, you can use a different ACME server by changing the
-   <literal><xref linkend="opt-security.acme.defaults.server" /></literal> option
-   to a provider of your choosing, or just change the server for one cert with
-   <literal><xref linkend="opt-security.acme.certs._name_.server" /></literal>.
-  </para>
-
-  <para>
-   You will need an HTTP server or DNS server for verification. For HTTP,
-   the server must have a webroot defined that can serve
-   <filename>.well-known/acme-challenge</filename>. This directory must be
-   writeable by the user that will run the ACME client. For DNS, you must
-   set up credentials with your provider/server for use with lego.
-  </para>
- </section>
- <section xml:id="module-security-acme-nginx">
-  <title>Using ACME certificates in Nginx</title>
-
-  <para>
-   NixOS supports fetching ACME certificates for you by setting
-   <literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link>
-   = true;</literal> in a virtualHost config. We first create self-signed
-   placeholder certificates in place of the real ACME certs. The placeholder
-   certs are overwritten when the ACME certs arrive. For
-   <literal>foo.example.com</literal> the config would look like this:
-  </para>
-
-<programlisting>
-<xref linkend="opt-security.acme.acceptTerms" /> = true;
-<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
-services.nginx = {
-  <link linkend="opt-services.nginx.enable">enable</link> = true;
-  <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
-    "foo.example.com" = {
-      <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
-      <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
-      # All serverAliases will be added as <link linkend="opt-security.acme.certs._name_.extraDomainNames">extra domain names</link> on the certificate.
-      <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "bar.example.com" ];
-      locations."/" = {
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www";
-      };
-    };
-
-    # We can also add a different vhost and reuse the same certificate
-    # but we have to append extraDomainNames manually beforehand:
-    # <link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."foo.example.com".extraDomainNames</link> = [ "baz.example.com" ];
-    "baz.example.com" = {
-      <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
-      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">useACMEHost</link> = "foo.example.com";
-      locations."/" = {
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www";
-      };
-    };
-  };
-}
-</programlisting>
- </section>
- <section xml:id="module-security-acme-httpd">
-  <title>Using ACME certificates in Apache/httpd</title>
-
-  <para>
-   Using ACME certificates with Apache virtual hosts is identical
-   to using them with Nginx. The attribute names are all the same, just replace
-   "nginx" with "httpd" where appropriate.
-  </para>
- </section>
- <section xml:id="module-security-acme-configuring">
-  <title>Manual configuration of HTTP-01 validation</title>
-
-  <para>
-   First off you will need to set up a virtual host to serve the challenges.
-   This example uses a vhost called <literal>certs.example.com</literal>, with
-   the intent that you will generate certs for all your vhosts and redirect
-   everyone to HTTPS.
-  </para>
-
-<programlisting>
-<xref linkend="opt-security.acme.acceptTerms" /> = true;
-<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
-
-# /var/lib/acme/.challenges must be writable by the ACME user
-# and readable by the Nginx user. The easiest way to achieve
-# this is to add the Nginx user to the ACME group.
-<link linkend="opt-users.users._name_.extraGroups">users.users.nginx.extraGroups</link> = [ "acme" ];
-
-services.nginx = {
-  <link linkend="opt-services.nginx.enable">enable</link> = true;
-  <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
-    "acmechallenge.example.com" = {
-      # Catchall vhost, will redirect users to HTTPS for all vhosts
-      <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ];
-      locations."/.well-known/acme-challenge" = {
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/lib/acme/.challenges";
-      };
-      locations."/" = {
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.return">return</link> = "301 https://$host$request_uri";
-      };
-    };
-  };
-}
-# Alternative config for Apache
-<link linkend="opt-users.users._name_.extraGroups">users.users.wwwrun.extraGroups</link> = [ "acme" ];
-services.httpd = {
-  <link linkend="opt-services.httpd.enable">enable = true;</link>
-  <link linkend="opt-services.httpd.virtualHosts">virtualHosts</link> = {
-    "acmechallenge.example.com" = {
-      # Catchall vhost, will redirect users to HTTPS for all vhosts
-      <link linkend="opt-services.httpd.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ];
-      # /var/lib/acme/.challenges must be writable by the ACME user and readable by the Apache user.
-      # By default, this is the case.
-      <link linkend="opt-services.httpd.virtualHosts._name_.documentRoot">documentRoot</link> = "/var/lib/acme/.challenges";
-      <link linkend="opt-services.httpd.virtualHosts._name_.extraConfig">extraConfig</link> = ''
-        RewriteEngine On
-        RewriteCond %{HTTPS} off
-        RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge [NC]
-        RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301]
-      '';
-    };
-  };
-}
-</programlisting>
-
-  <para>
-   Now you need to configure ACME to generate a certificate.
-  </para>
-
-<programlisting>
-<xref linkend="opt-security.acme.certs"/>."foo.example.com" = {
-  <link linkend="opt-security.acme.certs._name_.webroot">webroot</link> = "/var/lib/acme/.challenges";
-  <link linkend="opt-security.acme.certs._name_.email">email</link> = "foo@example.com";
-  # Ensure that the web server you use can read the generated certs
-  # Take a look at the <link linkend="opt-services.nginx.group">group</link> option for the web server you choose.
-  <link linkend="opt-security.acme.certs._name_.group">group</link> = "nginx";
-  # Since we have a wildcard vhost to handle port 80,
-  # we can generate certs for anything!
-  # Just make sure your DNS resolves them.
-  <link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> = [ "mail.example.com" ];
-};
-</programlisting>
-
-  <para>
-   The private key <filename>key.pem</filename> and certificate
-   <filename>fullchain.pem</filename> will be put into
-   <filename>/var/lib/acme/foo.example.com</filename>.
-  </para>
-
-  <para>
-   Refer to <xref linkend="ch-options" /> for all available configuration
-   options for the <link linkend="opt-security.acme.certs">security.acme</link>
-   module.
-  </para>
- </section>
- <section xml:id="module-security-acme-config-dns">
-  <title>Configuring ACME for DNS validation</title>
-
-  <para>
-   This is useful if you want to generate a wildcard certificate, since
-   ACME servers will only hand out wildcard certs over DNS validation.
-   There are a number of supported DNS providers and servers you can utilise,
-   see the <link xlink:href="https://go-acme.github.io/lego/dns/">lego docs</link>
-   for provider/server specific configuration values. For the sake of these
-   docs, we will provide a fully self-hosted example using bind.
-  </para>
-
-<programlisting>
-services.bind = {
-  <link linkend="opt-services.bind.enable">enable</link> = true;
-  <link linkend="opt-services.bind.extraConfig">extraConfig</link> = ''
-    include "/var/lib/secrets/dnskeys.conf";
-  '';
-  <link linkend="opt-services.bind.zones">zones</link> = [
-    rec {
-      name = "example.com";
-      file = "/var/db/bind/${name}";
-      master = true;
-      extraConfig = "allow-update { key rfc2136key.example.com.; };";
-    }
-  ];
-}
-
-# Now we can configure ACME
-<xref linkend="opt-security.acme.acceptTerms" /> = true;
-<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
-<xref linkend="opt-security.acme.certs" />."example.com" = {
-  <link linkend="opt-security.acme.certs._name_.domain">domain</link> = "*.example.com";
-  <link linkend="opt-security.acme.certs._name_.dnsProvider">dnsProvider</link> = "rfc2136";
-  <link linkend="opt-security.acme.certs._name_.credentialsFile">credentialsFile</link> = "/var/lib/secrets/certs.secret";
-  # We don't need to wait for propagation since this is a local DNS server
-  <link linkend="opt-security.acme.certs._name_.dnsPropagationCheck">dnsPropagationCheck</link> = false;
-};
-</programlisting>
-
-  <para>
-   The <filename>dnskeys.conf</filename> and <filename>certs.secret</filename>
-   must be kept secure and thus you should not keep their contents in your
-   Nix config. Instead, generate them one time with a systemd service:
-  </para>
-
-<programlisting>
-systemd.services.dns-rfc2136-conf = {
-  requiredBy = ["acme-example.com.service" "bind.service"];
-  before = ["acme-example.com.service" "bind.service"];
-  unitConfig = {
-    ConditionPathExists = "!/var/lib/secrets/dnskeys.conf";
-  };
-  serviceConfig = {
-    Type = "oneshot";
-    UMask = 0077;
-  };
-  path = [ pkgs.bind ];
-  script = ''
-    mkdir -p /var/lib/secrets
-    chmod 755 /var/lib/secrets
-    tsig-keygen rfc2136key.example.com &gt; /var/lib/secrets/dnskeys.conf
-    chown named:root /var/lib/secrets/dnskeys.conf
-    chmod 400 /var/lib/secrets/dnskeys.conf
-
-    # extract secret value from the dnskeys.conf
-    while read x y; do if [ "$x" = "secret" ]; then secret="''${y:1:''${#y}-3}"; fi; done &lt; /var/lib/secrets/dnskeys.conf
-
-    cat &gt; /var/lib/secrets/certs.secret &lt;&lt; EOF
-    RFC2136_NAMESERVER='127.0.0.1:53'
-    RFC2136_TSIG_ALGORITHM='hmac-sha256.'
-    RFC2136_TSIG_KEY='rfc2136key.example.com'
-    RFC2136_TSIG_SECRET='$secret'
-    EOF
-    chmod 400 /var/lib/secrets/certs.secret
-  '';
-};
-</programlisting>
-
-  <para>
-   Now you're all set to generate certs! You should monitor the first invocation
-   by running <literal>systemctl start acme-example.com.service &amp;
-   journalctl -fu acme-example.com.service</literal> and watching its log output.
-  </para>
- </section>
-
- <section xml:id="module-security-acme-config-dns-with-vhosts">
-  <title>Using DNS validation with web server virtual hosts</title>
-
-  <para>
-   It is possible to use DNS-01 validation with all certificates,
-   including those automatically configured via the Nginx/Apache
-   <literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link></literal>
-   option. This configuration pattern is fully
-   supported and part of the module's test suite for Nginx + Apache.
-  </para>
-
-  <para>
-   You must follow the guide above on configuring DNS-01 validation
-   first, however instead of setting the options for one certificate
-   (e.g. <xref linkend="opt-security.acme.certs._name_.dnsProvider" />)
-   you will set them as defaults
-   (e.g. <xref linkend="opt-security.acme.defaults.dnsProvider" />).
-  </para>
-
-<programlisting>
-# Configure ACME appropriately
-<xref linkend="opt-security.acme.acceptTerms" /> = true;
-<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
-<xref linkend="opt-security.acme.defaults" /> = {
-  <link linkend="opt-security.acme.defaults.dnsProvider">dnsProvider</link> = "rfc2136";
-  <link linkend="opt-security.acme.defaults.credentialsFile">credentialsFile</link> = "/var/lib/secrets/certs.secret";
-  # We don't need to wait for propagation since this is a local DNS server
-  <link linkend="opt-security.acme.defaults.dnsPropagationCheck">dnsPropagationCheck</link> = false;
-};
-
-# For each virtual host you would like to use DNS-01 validation with,
-# set acmeRoot = null
-services.nginx = {
-  <link linkend="opt-services.nginx.enable">enable</link> = true;
-  <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
-    "foo.example.com" = {
-      <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
-      <link linkend="opt-services.nginx.virtualHosts._name_.acmeRoot">acmeRoot</link> = null;
-    };
-  };
-}
-</programlisting>
-
-  <para>
-   And that's it! Next time your configuration is rebuilt, or when
-   you add a new virtualHost, it will be DNS-01 validated.
-  </para>
- </section>
-
- <section xml:id="module-security-acme-root-owned">
-  <title>Using ACME with services demanding root owned certificates</title>
-
-  <para>
-   Some services refuse to start if the configured certificate files
-   are not owned by root. PostgreSQL and OpenSMTPD are examples of these.
-   There is no way to change the user the ACME module uses (it will always be
-   <literal>acme</literal>), however you can use systemd's
-   <literal>LoadCredential</literal> feature to resolve this elegantly.
-   Below is an example configuration for OpenSMTPD, but this pattern
-   can be applied to any service.
-  </para>
-
-<programlisting>
-# Configure ACME however you like (DNS or HTTP validation), adding
-# the following configuration for the relevant certificate.
-# Note: You cannot use `systemctl reload` here as that would mean
-# the LoadCredential configuration below would be skipped and
-# the service would continue to use old certificates.
-security.acme.certs."mail.example.com".postRun = ''
-  systemctl restart opensmtpd
-'';
-
-# Now you must augment OpenSMTPD's systemd service to load
-# the certificate files.
-<link linkend="opt-systemd.services._name_.requires">systemd.services.opensmtpd.requires</link> = ["acme-finished-mail.example.com.target"];
-<link linkend="opt-systemd.services._name_.serviceConfig">systemd.services.opensmtpd.serviceConfig.LoadCredential</link> = let
-  certDir = config.security.acme.certs."mail.example.com".directory;
-in [
-  "cert.pem:${certDir}/cert.pem"
-  "key.pem:${certDir}/key.pem"
-];
-
-# Finally, configure OpenSMTPD to use these certs.
-services.opensmtpd = let
-  credsDir = "/run/credentials/opensmtpd.service";
-in {
-  enable = true;
-  setSendmail = false;
-  serverConfiguration = ''
-    pki mail.example.com cert "${credsDir}/cert.pem"
-    pki mail.example.com key "${credsDir}/key.pem"
-    listen on localhost tls pki mail.example.com
-    action act1 relay host smtp://127.0.0.1:10027
-    match for local action act1
-  '';
-};
-</programlisting>
- </section>
-
- <section xml:id="module-security-acme-regenerate">
-  <title>Regenerating certificates</title>
-
-  <para>
-   Should you need to regenerate a particular certificate in a hurry, such
-   as when a vulnerability is found in Let's Encrypt, there is now a convenient
-   mechanism for doing so. Running
-   <literal>systemctl clean --what=state acme-example.com.service</literal>
-   will remove all certificate files and the account data for the given domain,
-   allowing you to then <literal>systemctl start acme-example.com.service</literal>
-   to generate fresh ones.
-  </para>
- </section>
- <section xml:id="module-security-acme-fix-jws">
-  <title>Fixing JWS Verification error</title>
-
-  <para>
-   It is possible that your account credentials file may become corrupt and need
-   to be regenerated. In this scenario lego will produce the error <literal>JWS verification error</literal>.
-   The solution is to simply delete the associated accounts file and
-   re-run the affected service(s).
-  </para>
-
-<programlisting>
-# Find the accounts folder for the certificate
-systemctl cat acme-example.com.service | grep -Po 'accounts/[^:]*'
-export accountdir="$(!!)"
-# Move this folder to some place else
-mv /var/lib/acme/.lego/$accountdir{,.bak}
-# Recreate the folder using systemd-tmpfiles
-systemd-tmpfiles --create
-# Get a new account and reissue certificates
-# Note: Do this for all certs that share the same account email address
-systemctl start acme-example.com.service
-</programlisting>
-
- </section>
-</chapter>
diff --git a/nixos/modules/security/apparmor/includes.nix b/nixos/modules/security/apparmor/includes.nix
index f290e95a296..adfca04426c 100644
--- a/nixos/modules/security/apparmor/includes.nix
+++ b/nixos/modules/security/apparmor/includes.nix
@@ -22,7 +22,7 @@ in
 # some may even be completely useless.
 config.security.apparmor.includes = {
   # This one is included by <tunables/global>
-  # which is usualy included before any profile.
+  # which is usually included before any profile.
   "abstractions/tunables/alias" = ''
     alias /bin -> /run/current-system/sw/bin,
     alias /lib/modules -> /run/current-system/kernel/lib/modules,
diff --git a/nixos/modules/security/audit.nix b/nixos/modules/security/audit.nix
index 06b4766c8f5..afc7dd13039 100644
--- a/nixos/modules/security/audit.nix
+++ b/nixos/modules/security/audit.nix
@@ -57,7 +57,7 @@ in {
         type        = types.enum [ false true "lock" ];
         default     = false;
         description = lib.mdDoc ''
-          Whether to enable the Linux audit system. The special `lock' value can be used to
+          Whether to enable the Linux audit system. The special `lock` value can be used to
           enable auditing and prevent disabling it until a restart. Be careful about locking
           this, as it will prevent you from changing your audit configuration until you
           restart. If possible, test your configuration using build-vm beforehand.
diff --git a/nixos/modules/security/ca.nix b/nixos/modules/security/ca.nix
index c704e2c1f51..3cd56bff04d 100644
--- a/nixos/modules/security/ca.nix
+++ b/nixos/modules/security/ca.nix
@@ -18,6 +18,10 @@ in
 {
 
   options = {
+    security.pki.installCACerts = mkEnableOption "Add CA certificates to system" // {
+      default = true;
+      internal = true;
+    };
 
     security.pki.certificateFiles = mkOption {
       type = types.listOf types.path;
@@ -70,7 +74,7 @@ in
 
   };
 
-  config = {
+  config = mkIf cfg.installCACerts {
 
     # NixOS canonical location + Debian/Ubuntu/Arch/Gentoo compatibility.
     environment.etc."ssl/certs/ca-certificates.crt".source = caBundle;
diff --git a/nixos/modules/security/doas.nix b/nixos/modules/security/doas.nix
index 4d15ed9a802..115ca33efb5 100644
--- a/nixos/modules/security/doas.nix
+++ b/nixos/modules/security/doas.nix
@@ -19,7 +19,7 @@ let
   ];
 
   mkArgs = rule:
-    if (isNull rule.args) then ""
+    if (rule.args == null) then ""
     else if (length rule.args == 0) then "args"
     else "args ${concatStringsSep " " rule.args}";
 
@@ -27,9 +27,9 @@ let
     let
       opts = mkOpts rule;
 
-      as = optionalString (!isNull rule.runAs) "as ${rule.runAs}";
+      as = optionalString (rule.runAs != null) "as ${rule.runAs}";
 
-      cmd = optionalString (!isNull rule.cmd) "cmd ${rule.cmd}";
+      cmd = optionalString (rule.cmd != null) "cmd ${rule.cmd}";
 
       args = mkArgs rule;
     in
@@ -75,7 +75,9 @@ in
         {file}`/etc/doas.conf` file. More specific rules should
         come after more general ones in order to yield the expected behavior.
         You can use `mkBefore` and/or `mkAfter` to ensure
-        this is the case when configuration options are merged.
+        this is the case when configuration options are merged. Be aware that
+        this option cannot be used to override the behaviour allowing
+        passwordless operation for root.
       '';
       example = literalExpression ''
         [
@@ -224,7 +226,9 @@ in
       type = with types; lines;
       default = "";
       description = lib.mdDoc ''
-        Extra configuration text appended to {file}`doas.conf`.
+        Extra configuration text appended to {file}`doas.conf`. Be aware that
+        this option cannot be used to override the behaviour allowing
+        passwordless operation for root.
       '';
     };
   };
@@ -266,14 +270,14 @@ in
             # completely replace the contents of this file, use
             # `environment.etc."doas.conf"`.
 
-            # "root" is allowed to do anything.
-            permit nopass keepenv root
-
             # extraRules
             ${concatStringsSep "\n" (lists.flatten (map mkRule cfg.extraRules))}
 
             # extraConfig
             ${cfg.extraConfig}
+
+            # "root" is allowed to do anything.
+            permit nopass keepenv root
           '';
           preferLocalBuild = true;
         }
diff --git a/nixos/modules/security/ipa.nix b/nixos/modules/security/ipa.nix
new file mode 100644
index 00000000000..7075be95040
--- /dev/null
+++ b/nixos/modules/security/ipa.nix
@@ -0,0 +1,258 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+with lib; let
+  cfg = config.security.ipa;
+  pyBool = x:
+    if x
+    then "True"
+    else "False";
+
+  ldapConf = pkgs.writeText "ldap.conf" ''
+    # Turning this off breaks GSSAPI used with krb5 when rdns = false
+    SASL_NOCANON    on
+
+    URI ldaps://${cfg.server}
+    BASE ${cfg.basedn}
+    TLS_CACERT /etc/ipa/ca.crt
+  '';
+  nssDb =
+    pkgs.runCommand "ipa-nssdb"
+    {
+      nativeBuildInputs = [pkgs.nss.tools];
+    } ''
+      mkdir -p $out
+      certutil -d $out -N --empty-password
+      certutil -d $out -A --empty-password -n "${cfg.realm} IPA CA" -t CT,C,C -i ${cfg.certificate}
+    '';
+in {
+  options = {
+    security.ipa = {
+      enable = mkEnableOption (lib.mdDoc "FreeIPA domain integration");
+
+      certificate = mkOption {
+        type = types.package;
+        description = lib.mdDoc ''
+          IPA server CA certificate.
+
+          Use `nix-prefetch-url http://$server/ipa/config/ca.crt` to
+          obtain the file and the hash.
+        '';
+        example = literalExpression ''
+          pkgs.fetchurl {
+            url = http://ipa.example.com/ipa/config/ca.crt;
+            sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+          };
+        '';
+      };
+
+      domain = mkOption {
+        type = types.str;
+        example = "example.com";
+        description = lib.mdDoc "Domain of the IPA server.";
+      };
+
+      realm = mkOption {
+        type = types.str;
+        example = "EXAMPLE.COM";
+        description = lib.mdDoc "Kerberos realm.";
+      };
+
+      server = mkOption {
+        type = types.str;
+        example = "ipa.example.com";
+        description = lib.mdDoc "IPA Server hostname.";
+      };
+
+      basedn = mkOption {
+        type = types.str;
+        example = "dc=example,dc=com";
+        description = lib.mdDoc "Base DN to use when performing LDAP operations.";
+      };
+
+      offlinePasswords = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Whether to store offline passwords when the server is down.";
+      };
+
+      cacheCredentials = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Whether to cache credentials.";
+      };
+
+      ifpAllowedUids = mkOption {
+        type = types.listOf types.string;
+        default = ["root"];
+        description = lib.mdDoc "A list of users allowed to access the ifp dbus interface.";
+      };
+
+      dyndns = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = lib.mdDoc "Whether to enable FreeIPA automatic hostname updates.";
+        };
+
+        interface = mkOption {
+          type = types.str;
+          example = "eth0";
+          default = "*";
+          description = lib.mdDoc "Network interface to perform hostname updates through.";
+        };
+      };
+
+      chromiumSupport = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Whether to whitelist the FreeIPA domain in Chromium.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !config.krb5.enable;
+        message = "krb5 must be disabled through `krb5.enable` for FreeIPA integration to work.";
+      }
+      {
+        assertion = !config.users.ldap.enable;
+        message = "ldap must be disabled through `users.ldap.enable` for FreeIPA integration to work.";
+      }
+    ];
+
+    environment.systemPackages = with pkgs; [krb5Full freeipa];
+
+    environment.etc = {
+      "ipa/default.conf".text = ''
+        [global]
+        basedn = ${cfg.basedn}
+        realm = ${cfg.realm}
+        domain = ${cfg.domain}
+        server = ${cfg.server}
+        host = ${config.networking.hostName}
+        xmlrpc_uri = https://${cfg.server}/ipa/xml
+        enable_ra = True
+      '';
+
+      "ipa/nssdb".source = nssDb;
+
+      "krb5.conf".text = ''
+        [libdefaults]
+         default_realm = ${cfg.realm}
+         dns_lookup_realm = false
+         dns_lookup_kdc = true
+         rdns = false
+         ticket_lifetime = 24h
+         forwardable = true
+         udp_preference_limit = 0
+
+        [realms]
+         ${cfg.realm} = {
+          kdc = ${cfg.server}:88
+          master_kdc = ${cfg.server}:88
+          admin_server = ${cfg.server}:749
+          default_domain = ${cfg.domain}
+          pkinit_anchors = FILE:/etc/ipa/ca.crt
+        }
+
+        [domain_realm]
+         .${cfg.domain} = ${cfg.realm}
+         ${cfg.domain} = ${cfg.realm}
+         ${cfg.server} = ${cfg.realm}
+
+        [dbmodules]
+          ${cfg.realm} = {
+            db_library = ${pkgs.freeipa}/lib/krb5/plugins/kdb/ipadb.so
+          }
+      '';
+
+      "openldap/ldap.conf".source = ldapConf;
+    };
+
+    environment.etc."chromium/policies/managed/freeipa.json" = mkIf cfg.chromiumSupport {
+      text = ''
+        { "AuthServerWhitelist": "*.${cfg.domain}" }
+      '';
+    };
+
+    system.activationScripts.ipa = stringAfter ["etc"] ''
+      # libcurl requires a hard copy of the certificate
+      if ! ${pkgs.diffutils}/bin/diff ${cfg.certificate} /etc/ipa/ca.crt > /dev/null 2>&1; then
+        rm -f /etc/ipa/ca.crt
+        cp ${cfg.certificate} /etc/ipa/ca.crt
+      fi
+
+      if [ ! -f /etc/krb5.keytab ]; then
+        cat <<EOF
+
+          In order to complete FreeIPA integration, please join the domain by completing the following steps:
+          1. Authenticate as an IPA user authorized to join new hosts, e.g. kinit admin@${cfg.realm}
+          2. Join the domain and obtain the keytab file: ipa-join
+          3. Install the keytab file: sudo install -m 600 krb5.keytab /etc/
+          4. Restart sssd systemd service: sudo systemctl restart sssd
+
+      EOF
+      fi
+    '';
+
+    services.sssd.config = ''
+      [domain/${cfg.domain}]
+      id_provider = ipa
+      auth_provider = ipa
+      access_provider = ipa
+      chpass_provider = ipa
+
+      ipa_domain = ${cfg.domain}
+      ipa_server = _srv_, ${cfg.server}
+      ipa_hostname = ${config.networking.hostName}.${cfg.domain}
+
+      cache_credentials = ${pyBool cfg.cacheCredentials}
+      krb5_store_password_if_offline = ${pyBool cfg.offlinePasswords}
+      ${optionalString ((toLower cfg.domain) != (toLower cfg.realm))
+        "krb5_realm = ${cfg.realm}"}
+
+      dyndns_update = ${pyBool cfg.dyndns.enable}
+      dyndns_iface = ${cfg.dyndns.interface}
+
+      ldap_tls_cacert = /etc/ipa/ca.crt
+      ldap_user_extra_attrs = mail:mail, sn:sn, givenname:givenname, telephoneNumber:telephoneNumber, lock:nsaccountlock
+
+      [sssd]
+      debug_level = 65510
+      services = nss, sudo, pam, ssh, ifp
+      domains = ${cfg.domain}
+
+      [nss]
+      homedir_substring = /home
+
+      [pam]
+      pam_pwd_expiration_warning = 3
+      pam_verbosity = 3
+
+      [sudo]
+      debug_level = 65510
+
+      [autofs]
+
+      [ssh]
+
+      [pac]
+
+      [ifp]
+      user_attributes = +mail, +telephoneNumber, +givenname, +sn, +lock
+      allowed_uids = ${concatStringsSep ", " cfg.ifpAllowedUids}
+    '';
+
+    services.ntp.servers = singleton cfg.server;
+    services.sssd.enable = true;
+    services.ntp.enable = true;
+
+    security.pki.certificateFiles = singleton cfg.certificate;
+  };
+}
diff --git a/nixos/modules/security/lock-kernel-modules.nix b/nixos/modules/security/lock-kernel-modules.nix
index 674ba857818..333b6480142 100644
--- a/nixos/modules/security/lock-kernel-modules.nix
+++ b/nixos/modules/security/lock-kernel-modules.nix
@@ -22,12 +22,11 @@ with lib;
 
   config = mkIf config.security.lockKernelModules {
     boot.kernelModules = concatMap (x:
-      if x.device != null
-        then
-          if x.fsType == "vfat"
-            then [ "vfat" "nls-cp437" "nls-iso8859-1" ]
-            else [ x.fsType ]
-        else []) config.system.build.fileSystems;
+      optionals (x.device != null) (
+        if x.fsType == "vfat"
+        then [ "vfat" "nls-cp437" "nls-iso8859-1" ]
+        else [ x.fsType ])
+      ) config.system.build.fileSystems;
 
     systemd.services.disable-kernel-module-loading = {
       description = "Disable kernel module loading";
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index 273bc796341..ac9da4a823b 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -446,6 +446,15 @@ let
         };
       };
 
+      zfs = mkOption {
+        default = config.security.pam.zfs.enable;
+        defaultText = literalExpression "config.security.pam.zfs.enable";
+        type = types.bool;
+        description = lib.mdDoc ''
+          Enable unlocking and mounting of encrypted ZFS home dataset at login.
+        '';
+      };
+
       text = mkOption {
         type = types.nullOr types.lines;
         description = lib.mdDoc "Contents of the PAM service file.";
@@ -475,6 +484,9 @@ let
           optionalString cfg.mysqlAuth ''
             account sufficient ${pkgs.pam_mysql}/lib/security/pam_mysql.so config_file=/etc/security/pam_mysql.conf
           '' +
+          optionalString (config.services.kanidm.enablePam) ''
+            account sufficient ${pkgs.kanidm}/lib/pam_kanidm.so ignore_unknown_user
+          '' +
           optionalString (config.services.sssd.enable && cfg.sssdStrictAccess==false) ''
             account sufficient ${pkgs.sssd}/lib/security/pam_sss.so
           '' +
@@ -488,6 +500,9 @@ let
             account [success=ok ignore=ignore default=die] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_login.so
             account [success=ok default=ignore] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_admin.so
           '' +
+          optionalString config.services.homed.enable ''
+            account sufficient ${config.systemd.package}/lib/security/pam_systemd_home.so
+          '' +
           # The required pam_unix.so module has to come after all the sufficient modules
           # because otherwise, the account lookup will fail if the user does not exist
           # locally, for example with MySQL- or LDAP-auth.
@@ -533,6 +548,9 @@ let
           (let yubi = config.security.pam.yubico; in optionalString cfg.yubicoAuth ''
             auth ${yubi.control} ${pkgs.yubico-pam}/lib/security/pam_yubico.so mode=${toString yubi.mode} ${optionalString (yubi.challengeResponsePath != null) "chalresp_path=${yubi.challengeResponsePath}"} ${optionalString (yubi.mode == "client") "id=${toString yubi.id}"} ${optionalString yubi.debug "debug"}
           '') +
+          (let dp9ik = config.security.pam.dp9ik; in optionalString dp9ik.enable ''
+            auth ${dp9ik.control} ${pkgs.pam_dp9ik}/lib/security/pam_p9.so ${dp9ik.authserver}
+          '') +
           optionalString cfg.fprintAuth ''
             auth sufficient ${pkgs.fprintd}/lib/security/pam_fprintd.so
           '' +
@@ -541,8 +559,10 @@ let
           # after it succeeds. Certain modules need to run after pam_unix
           # prompts the user for password so we run it once with 'optional' at an
           # earlier point and it will run again with 'sufficient' further down.
-          # We use try_first_pass the second time to avoid prompting password twice
-          (optionalString (cfg.unixAuth &&
+          # We use try_first_pass the second time to avoid prompting password twice.
+          #
+          # The same principle applies to systemd-homed
+          (optionalString ((cfg.unixAuth || config.services.homed.enable) &&
             (config.security.pam.enableEcryptfs
               || config.security.pam.enableFscrypt
               || cfg.pamMount
@@ -551,9 +571,13 @@ let
               || cfg.googleAuthenticator.enable
               || cfg.gnupg.enable
               || cfg.failDelay.enable
-              || cfg.duoSecurity.enable))
+              || cfg.duoSecurity.enable
+              || cfg.zfs))
             (
-              ''
+              optionalString config.services.homed.enable ''
+                auth optional ${config.systemd.package}/lib/security/pam_systemd_home.so
+              '' +
+              optionalString cfg.unixAuth ''
                 auth optional pam_unix.so ${optionalString cfg.allowNullPassword "nullok"} ${optionalString cfg.nodelay "nodelay"} likeauth
               '' +
               optionalString config.security.pam.enableEcryptfs ''
@@ -562,6 +586,9 @@ let
               optionalString config.security.pam.enableFscrypt ''
                 auth optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so
               '' +
+              optionalString cfg.zfs ''
+                auth optional ${config.boot.zfs.package}/lib/security/pam_zfs_key.so homes=${config.security.pam.zfs.homes}
+              '' +
               optionalString cfg.pamMount ''
                 auth optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive
               '' +
@@ -584,6 +611,9 @@ let
                 auth required ${pkgs.duo-unix}/lib/security/pam_duo.so
               ''
             )) +
+          optionalString config.services.homed.enable ''
+            auth sufficient ${config.systemd.package}/lib/security/pam_systemd_home.so
+          '' +
           optionalString cfg.unixAuth ''
             auth sufficient pam_unix.so ${optionalString cfg.allowNullPassword "nullok"} ${optionalString cfg.nodelay "nodelay"} likeauth try_first_pass
           '' +
@@ -593,6 +623,9 @@ let
           optionalString use_ldap ''
             auth sufficient ${pam_ldap}/lib/security/pam_ldap.so use_first_pass
           '' +
+          optionalString config.services.kanidm.enablePam ''
+            auth sufficient ${pkgs.kanidm}/lib/pam_kanidm.so ignore_unknown_user use_first_pass
+          '' +
           optionalString config.services.sssd.enable ''
             auth sufficient ${pkgs.sssd}/lib/security/pam_sss.so use_first_pass
           '' +
@@ -605,7 +638,11 @@ let
             auth required pam_deny.so
 
             # Password management.
-            password sufficient pam_unix.so nullok sha512
+          '' +
+          optionalString config.services.homed.enable ''
+            password sufficient ${config.systemd.package}/lib/security/pam_systemd_home.so
+          '' + ''
+            password sufficient pam_unix.so nullok yescrypt
           '' +
           optionalString config.security.pam.enableEcryptfs ''
             password optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so
@@ -613,6 +650,9 @@ let
           optionalString config.security.pam.enableFscrypt ''
             password optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so
           '' +
+          optionalString cfg.zfs ''
+            password optional ${config.boot.zfs.package}/lib/security/pam_zfs_key.so homes=${config.security.pam.zfs.homes}
+          '' +
           optionalString cfg.pamMount ''
             password optional ${pkgs.pam_mount}/lib/security/pam_mount.so
           '' +
@@ -622,8 +662,11 @@ let
           optionalString cfg.mysqlAuth ''
             password sufficient ${pkgs.pam_mysql}/lib/security/pam_mysql.so config_file=/etc/security/pam_mysql.conf
           '' +
+          optionalString config.services.kanidm.enablePam ''
+            password sufficient ${pkgs.kanidm}/lib/pam_kanidm.so
+          '' +
           optionalString config.services.sssd.enable ''
-            password sufficient ${pkgs.sssd}/lib/security/pam_sss.so use_authtok
+            password sufficient ${pkgs.sssd}/lib/security/pam_sss.so
           '' +
           optionalString config.security.pam.krb5.enable ''
             password sufficient ${pam_krb5}/lib/security/pam_krb5.so use_first_pass
@@ -650,6 +693,9 @@ let
           ++ optional (cfg.ttyAudit.enablePattern != null) "enable=${cfg.ttyAudit.enablePattern}"
           ++ optional (cfg.ttyAudit.disablePattern != null) "disable=${cfg.ttyAudit.disablePattern}"
           )) +
+          optionalString config.services.homed.enable ''
+            session required ${config.systemd.package}/lib/security/pam_systemd_home.so
+          '' +
           optionalString cfg.makeHomeDir ''
             session required ${pkgs.pam}/lib/security/pam_mkhomedir.so silent skel=${config.security.pam.makeHomeDir.skelDirectory} umask=0077
           '' +
@@ -667,6 +713,10 @@ let
             session [success=1 default=ignore] pam_succeed_if.so service = systemd-user
             session optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so
           '' +
+          optionalString cfg.zfs ''
+            session [success=1 default=ignore] pam_succeed_if.so service = systemd-user
+            session optional ${config.boot.zfs.package}/lib/security/pam_zfs_key.so homes=${config.security.pam.zfs.homes} ${optionalString config.security.pam.zfs.noUnmount "nounmount"}
+          '' +
           optionalString cfg.pamMount ''
             session optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive
           '' +
@@ -676,6 +726,9 @@ let
           optionalString cfg.mysqlAuth ''
             session optional ${pkgs.pam_mysql}/lib/security/pam_mysql.so config_file=/etc/security/pam_mysql.conf
           '' +
+          optionalString config.services.kanidm.enablePam ''
+            session optional ${pkgs.kanidm}/lib/pam_kanidm.so
+          '' +
           optionalString config.services.sssd.enable ''
             session optional ${pkgs.sssd}/lib/security/pam_sss.so
           '' +
@@ -775,7 +828,7 @@ let
     };
   }));
 
-  motd = if isNull config.users.motdFile
+  motd = if config.users.motdFile == null
          then pkgs.writeText "motd" config.users.motd
          else config.users.motdFile;
 
@@ -863,6 +916,32 @@ in
 
     security.pam.enableOTPW = mkEnableOption (lib.mdDoc "the OTPW (one-time password) PAM module");
 
+    security.pam.dp9ik = {
+      enable = mkEnableOption (
+        lib.mdDoc ''
+          the dp9ik pam module provided by tlsclient.
+
+          If set, users can be authenticated against the 9front
+          authentication server given in {option}`security.pam.dp9ik.authserver`.
+        ''
+      );
+      control = mkOption {
+        default = "sufficient";
+        type = types.str;
+        description = lib.mdDoc ''
+          This option sets the pam "control" used for this module.
+        '';
+      };
+      authserver = mkOption {
+        default = null;
+        type = with types; nullOr string;
+        description = lib.mdDoc ''
+          This controls the hostname for the 9front authentication server
+          that users will be authenticated against.
+        '';
+      };
+    };
+
     security.pam.krb5 = {
       enable = mkOption {
         default = config.krb5.enable;
@@ -1184,6 +1263,34 @@ in
       };
     };
 
+    security.pam.zfs = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = lib.mdDoc ''
+          Enable unlocking and mounting of encrypted ZFS home dataset at login.
+        '';
+      };
+
+      homes = mkOption {
+        example = "rpool/home";
+        default = "rpool/home";
+        type = types.str;
+        description = lib.mdDoc ''
+          Prefix of home datasets. This value will be concatenated with
+          `"/" + <username>` in order to determine the home dataset to unlock.
+        '';
+      };
+
+      noUnmount = mkOption {
+        default = false;
+        type = types.bool;
+        description = lib.mdDoc ''
+          Do not unmount home dataset on logout.
+        '';
+      };
+    };
+
     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.
@@ -1215,17 +1322,24 @@ in
   config = {
     assertions = [
       {
-        assertion = isNull config.users.motd || isNull config.users.motdFile;
+        assertion = config.users.motd == null || config.users.motdFile == null;
         message = ''
           Only one of users.motd and users.motdFile can be set.
         '';
       }
+      {
+        assertion = config.security.pam.zfs.enable -> (config.boot.zfs.enabled || config.boot.zfs.enableUnstable);
+        message = ''
+          `security.pam.zfs.enable` requires enabling ZFS (`boot.zfs.enabled` or `boot.zfs.enableUnstable`).
+        '';
+      }
     ];
 
     environment.systemPackages =
       # Include the PAM modules in the system path mostly for the manpages.
       [ pkgs.pam ]
       ++ optional config.users.ldap.enable pam_ldap
+      ++ optional config.services.kanidm.enablePam pkgs.kanidm
       ++ optional config.services.sssd.enable pkgs.sssd
       ++ optionals config.security.pam.krb5.enable [pam_krb5 pam_ccreds]
       ++ optionals config.security.pam.enableOTPW [ pkgs.otpw ]
@@ -1292,6 +1406,9 @@ in
       optionalString use_ldap ''
          mr ${pam_ldap}/lib/security/pam_ldap.so,
       '' +
+      optionalString config.services.kanidm.enablePam ''
+        mr ${pkgs.kanidm}/lib/pam_kanidm.so,
+      '' +
       optionalString config.services.sssd.enable ''
         mr ${pkgs.sssd}/lib/security/pam_sss.so,
       '' +
@@ -1360,7 +1477,13 @@ in
         mr ${pkgs.plasma5Packages.kwallet-pam}/lib/security/pam_kwallet5.so,
       '' +
       optionalString config.virtualisation.lxc.lxcfs.enable ''
-        mr ${pkgs.lxc}/lib/security/pam_cgfs.so
+        mr ${pkgs.lxc}/lib/security/pam_cgfs.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.zfs)) ''
+        mr ${config.boot.zfs.package}/lib/security/pam_zfs_key.so,
+      '' +
+      optionalString config.services.homed.enable ''
+        mr ${config.systemd.package}/lib/security/pam_systemd_home.so
       '';
   };
 
diff --git a/nixos/modules/security/pam_mount.nix b/nixos/modules/security/pam_mount.nix
index 481f1f3d38e..ad78f38b086 100644
--- a/nixos/modules/security/pam_mount.nix
+++ b/nixos/modules/security/pam_mount.nix
@@ -47,6 +47,18 @@ in
         '';
       };
 
+      cryptMountOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = literalExpression ''
+          [ "allow_discard" ]
+        '';
+        description = lib.mdDoc ''
+          Global mount options that apply to every crypt volume.
+          You can define volume-specific options in the volume definitions.
+        '';
+      };
+
       fuseMountOptions = mkOption {
         type = types.listOf types.str;
         default = [];
@@ -155,9 +167,11 @@ in
           <!-- create mount point if not present -->
           <mkmountpoint enable="${if cfg.createMountPoints then "1" else "0"}" remove="${if cfg.removeCreatedMountPoints then "true" else "false"}" />
           <!-- specify the binaries to be called -->
-          <fusemount>${pkgs.fuse}/bin/mount.fuse %(VOLUME) %(MNTPT) -o ${concatStringsSep "," (cfg.fuseMountOptions ++ [ "%(OPTIONS)" ])}</fusemount>
+          <!-- the comma in front of the options is necessary for empty options -->
+          <fusemount>${pkgs.fuse}/bin/mount.fuse %(VOLUME) %(MNTPT) -o ,${concatStringsSep "," (cfg.fuseMountOptions ++ [ "%(OPTIONS)" ])}'</fusemount>
           <fuseumount>${pkgs.fuse}/bin/fusermount -u %(MNTPT)</fuseumount>
-          <cryptmount>${pkgs.pam_mount}/bin/mount.crypt %(VOLUME) %(MNTPT)</cryptmount>
+          <!-- the comma in front of the options is necessary for empty options -->
+          <cryptmount>${pkgs.pam_mount}/bin/mount.crypt -o ,${concatStringsSep "," (cfg.cryptMountOptions ++ [ "%(OPTIONS)" ])} %(VOLUME) %(MNTPT)</cryptmount>
           <cryptumount>${pkgs.pam_mount}/bin/umount.crypt %(MNTPT)</cryptumount>
           <pmvarrun>${pkgs.pam_mount}/bin/pmvarrun -u %(USER) -o %(OPERATION)</pmvarrun>
           ${optionalString oflRequired "<ofl>${fake_ofl}/bin/fake_ofl %(SIGNAL) %(MNTPT)</ofl>"}
diff --git a/nixos/modules/security/polkit.nix b/nixos/modules/security/polkit.nix
index f33898578b8..de427ccb295 100644
--- a/nixos/modules/security/polkit.nix
+++ b/nixos/modules/security/polkit.nix
@@ -14,7 +14,7 @@ in
 
     security.polkit.enable = mkEnableOption (lib.mdDoc "polkit");
 
-    security.polkit.debug = mkEnableOption (lib.mdDoc "debug logs from polkit. This is required in order to see log messages from rule definitions.");
+    security.polkit.debug = mkEnableOption (lib.mdDoc "debug logs from polkit. This is required in order to see log messages from rule definitions");
 
     security.polkit.extraConfig = mkOption {
       type = types.lines;
diff --git a/nixos/modules/security/sudo.nix b/nixos/modules/security/sudo.nix
index 296b61fd703..9ac91bd0d36 100644
--- a/nixos/modules/security/sudo.nix
+++ b/nixos/modules/security/sudo.nix
@@ -216,10 +216,10 @@ in
         ${concatStringsSep "\n" (
           lists.flatten (
             map (
-              rule: if (length rule.commands != 0) then [
+              rule: optionals (length rule.commands != 0) [
                 (map (user: "${toUserString user}	${rule.host}=(${rule.runAs})	${toCommandsString rule.commands}") rule.users)
                 (map (group: "${toGroupString group}	${rule.host}=(${rule.runAs})	${toCommandsString rule.commands}") rule.groups)
-              ] else []
+              ]
             ) cfg.extraRules
           )
         )}
diff --git a/nixos/modules/security/systemd-confinement.nix b/nixos/modules/security/systemd-confinement.nix
index be04741f4d0..cdf6c22ef1b 100644
--- a/nixos/modules/security/systemd-confinement.nix
+++ b/nixos/modules/security/systemd-confinement.nix
@@ -94,7 +94,6 @@ in {
       };
 
       config = let
-        rootName = "${mkPathSafeName name}-chroot";
         inherit (config.confinement) binSh fullUnit;
         wantsAPIVFS = lib.mkDefault (config.confinement.mode == "full-apivfs");
       in lib.mkIf config.confinement.enable {
diff --git a/nixos/modules/security/tpm2.nix b/nixos/modules/security/tpm2.nix
index 5a023cec48e..708c3a69d17 100644
--- a/nixos/modules/security/tpm2.nix
+++ b/nixos/modules/security/tpm2.nix
@@ -3,7 +3,7 @@ let
   cfg = config.security.tpm2;
 
   # This snippet is taken from tpm2-tss/dist/tpm-udev.rules, but modified to allow custom user/groups
-  # The idea is that the tssUser is allowed to acess the TPM and kernel TPM resource manager, while
+  # The idea is that the tssUser is allowed to access the TPM and kernel TPM resource manager, while
   # the tssGroup is only allowed to access the kernel resource manager
   # Therefore, if either of the two are null, the respective part isn't generated
   udevRules = tssUser: tssGroup: ''
diff --git a/nixos/modules/security/wrappers/default.nix b/nixos/modules/security/wrappers/default.nix
index 4b62abd658a..12255d8392f 100644
--- a/nixos/modules/security/wrappers/default.nix
+++ b/nixos/modules/security/wrappers/default.nix
@@ -283,7 +283,7 @@ in
         '';
 
     ###### wrappers consistency checks
-    system.extraDependencies = lib.singleton (pkgs.runCommandLocal
+    system.checks = lib.singleton (pkgs.runCommandLocal
       "ensure-all-wrappers-paths-exist" { }
       ''
         # make sure we produce output
diff --git a/nixos/modules/services/amqp/activemq/default.nix b/nixos/modules/services/amqp/activemq/default.nix
index bd37fe3b557..b1f9b7a3bb1 100644
--- a/nixos/modules/services/amqp/activemq/default.nix
+++ b/nixos/modules/services/amqp/activemq/default.nix
@@ -7,20 +7,19 @@ let
 
   cfg = config.services.activemq;
 
-  activemqBroker = stdenv.mkDerivation {
-    name = "activemq-broker";
-    phases = [ "installPhase" ];
-    buildInputs = [ jdk ];
-    installPhase = ''
-      mkdir -p $out/lib
-      source ${activemq}/lib/classpath.env
-      export CLASSPATH
-      ln -s "${./ActiveMQBroker.java}" ActiveMQBroker.java
-      javac -d $out/lib ActiveMQBroker.java
-    '';
-  };
+  activemqBroker = runCommand "activemq-broker"
+    {
+      nativeBuildInputs = [ jdk ];
+    } ''
+    mkdir -p $out/lib
+    source ${activemq}/lib/classpath.env
+    export CLASSPATH
+    ln -s "${./ActiveMQBroker.java}" ActiveMQBroker.java
+    javac -d $out/lib ActiveMQBroker.java
+  '';
 
-in {
+in
+{
 
   options = {
     services.activemq = {
diff --git a/nixos/modules/services/audio/gmediarender.nix b/nixos/modules/services/audio/gmediarender.nix
new file mode 100644
index 00000000000..2f23232d19c
--- /dev/null
+++ b/nixos/modules/services/audio/gmediarender.nix
@@ -0,0 +1,116 @@
+{ pkgs, lib, config, utils, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gmediarender;
+in
+{
+  options.services.gmediarender = {
+    enable = mkEnableOption (mdDoc "the gmediarender DLNA renderer");
+
+    audioDevice = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = mdDoc ''
+        The audio device to use.
+      '';
+    };
+
+    audioSink = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = mdDoc ''
+        The audio sink to use.
+      '';
+    };
+
+    friendlyName = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = mdDoc ''
+        A "friendly name" for identifying the endpoint.
+      '';
+    };
+
+    initialVolume = mkOption {
+      type = types.nullOr types.int;
+      default = 0;
+      description = mdDoc ''
+        A default volume attenuation (in dB) for the endpoint.
+      '';
+    };
+
+    package = mkPackageOptionMD pkgs "gmediarender" {
+      default = "gmrender-resurrect";
+    };
+
+    port = mkOption {
+      type = types.nullOr types.port;
+      default = null;
+      description = mdDoc "Port that will be used to accept client connections.";
+    };
+
+    uuid = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = mdDoc ''
+        A UUID for uniquely identifying the endpoint.  If you have
+        multiple renderers on your network, you MUST set this.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd = {
+      services.gmediarender = {
+        after = [ "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+        description = "gmediarender server daemon";
+        environment = {
+          XDG_CACHE_HOME = "%t/gmediarender";
+        };
+        serviceConfig = {
+          DynamicUser = true;
+          User = "gmediarender";
+          Group = "gmediarender";
+          SupplementaryGroups = [ "audio" ];
+          ExecStart =
+            "${cfg.package}/bin/gmediarender " +
+            optionalString (cfg.audioDevice != null) ("--gstout-audiodevice=${utils.escapeSystemdExecArg cfg.audioDevice} ") +
+            optionalString (cfg.audioSink != null) ("--gstout-audiosink=${utils.escapeSystemdExecArg cfg.audioSink} ") +
+            optionalString (cfg.friendlyName != null) ("--friendly-name=${utils.escapeSystemdExecArg cfg.friendlyName} ") +
+            optionalString (cfg.initialVolume != 0) ("--initial-volume=${toString cfg.initialVolume} ") +
+            optionalString (cfg.port != null) ("--port=${toString cfg.port} ") +
+            optionalString (cfg.uuid != null) ("--uuid=${utils.escapeSystemdExecArg cfg.uuid} ");
+          Restart = "always";
+          RuntimeDirectory = "gmediarender";
+
+          # Security options:
+          CapabilityBoundingSet = "";
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
+          # PrivateDevices = true;
+          PrivateTmp = true;
+          PrivateUsers = true;
+          ProcSubset = "pid";
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [ "@system-service" "~@privileged" ];
+          UMask = 066;
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/audio/gonic.nix b/nixos/modules/services/audio/gonic.nix
new file mode 100644
index 00000000000..65cf10f2c4b
--- /dev/null
+++ b/nixos/modules/services/audio/gonic.nix
@@ -0,0 +1,89 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gonic;
+  settingsFormat = pkgs.formats.keyValue {
+    mkKeyValue = lib.generators.mkKeyValueDefault { } " ";
+    listsAsDuplicateKeys = true;
+  };
+in
+{
+  options = {
+    services.gonic = {
+
+      enable = mkEnableOption (lib.mdDoc "Gonic music server");
+
+      settings = mkOption rec {
+        type = settingsFormat.type;
+        apply = recursiveUpdate default;
+        default = {
+          listen-addr = "127.0.0.1:4747";
+          cache-path = "/var/cache/gonic";
+          tls-cert = null;
+          tls-key = null;
+        };
+        example = {
+          music-path = [ "/mnt/music" ];
+          podcast-path = "/mnt/podcasts";
+        };
+        description = lib.mdDoc ''
+          Configuration for Gonic, see <https://github.com/sentriz/gonic#configuration-options> for supported values.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.gonic = {
+      description = "Gonic Media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart =
+          let
+            # these values are null by default but should not appear in the final config
+            filteredSettings = filterAttrs (n: v: !((n == "tls-cert" || n == "tls-key") && v == null)) cfg.settings;
+          in
+          "${pkgs.gonic}/bin/gonic -config-path ${settingsFormat.generate "gonic" filteredSettings}";
+        DynamicUser = true;
+        StateDirectory = "gonic";
+        CacheDirectory = "gonic";
+        WorkingDirectory = "/var/lib/gonic";
+        RuntimeDirectory = "gonic";
+        RootDirectory = "/run/gonic";
+        ReadWritePaths = "";
+        BindReadOnlyPaths = [
+          # gonic can access scrobbling services
+          "-/etc/ssl/certs/ca-certificates.crt"
+          builtins.storeDir
+          cfg.settings.podcast-path
+        ] ++ cfg.settings.music-path
+        ++ lib.optional (cfg.settings.tls-cert != null) cfg.settings.tls-cert
+        ++ lib.optional (cfg.settings.tls-key != null) cfg.settings.tls-key;
+        CapabilityBoundingSet = "";
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
+        RestrictRealtime = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        UMask = "0066";
+        ProtectHostname = true;
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.autrimpo ];
+}
diff --git a/nixos/modules/services/audio/hqplayerd.nix b/nixos/modules/services/audio/hqplayerd.nix
index eff1549380c..d54400b18e3 100644
--- a/nixos/modules/services/audio/hqplayerd.nix
+++ b/nixos/modules/services/audio/hqplayerd.nix
@@ -82,7 +82,6 @@ in
       etc = {
         "hqplayer/hqplayerd.xml" = mkIf (cfg.config != null) { source = pkgs.writeText "hqplayerd.xml" cfg.config; };
         "hqplayer/hqplayerd4-key.xml" = mkIf (cfg.licenseFile != null) { source = cfg.licenseFile; };
-        "modules-load.d/taudio2.conf".source = "${pkg}/etc/modules-load.d/taudio2.conf";
       };
       systemPackages = [ pkg ];
     };
@@ -91,8 +90,6 @@ in
       allowedTCPPorts = [ 8088 4321 ];
     };
 
-    services.udev.packages = [ pkg ];
-
     systemd = {
       tmpfiles.rules = [
         "d ${configDir}      0755 hqplayer hqplayer - -"
diff --git a/nixos/modules/services/audio/mpd.nix b/nixos/modules/services/audio/mpd.nix
index ba1e4716c9b..3c853973c87 100644
--- a/nixos/modules/services/audio/mpd.nix
+++ b/nixos/modules/services/audio/mpd.nix
@@ -102,7 +102,7 @@ in {
           Extra directives added to to the end of MPD's configuration file,
           mpd.conf. Basic configuration like file location and uid/gid
           is added automatically to the beginning of the file. For available
-          options see `man 5 mpd.conf`'.
+          options see {manpage}`mpd.conf(5)`.
         '';
       };
 
diff --git a/nixos/modules/services/audio/navidrome.nix b/nixos/modules/services/audio/navidrome.nix
index e73828081d4..e18e61eb6d4 100644
--- a/nixos/modules/services/audio/navidrome.nix
+++ b/nixos/modules/services/audio/navidrome.nix
@@ -11,6 +11,8 @@ in {
 
       enable = mkEnableOption (lib.mdDoc "Navidrome music server");
 
+      package = mkPackageOptionMD pkgs "navidrome" { };
+
       settings = mkOption rec {
         type = settingsFormat.type;
         apply = recursiveUpdate default;
@@ -36,7 +38,7 @@ in {
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.navidrome}/bin/navidrome --configfile ${settingsFormat.generate "navidrome.json" cfg.settings}
+          ${cfg.package}/bin/navidrome --configfile ${settingsFormat.generate "navidrome.json" cfg.settings}
         '';
         DynamicUser = true;
         StateDirectory = "navidrome";
diff --git a/nixos/modules/services/audio/roon-bridge.nix b/nixos/modules/services/audio/roon-bridge.nix
index e9335091ba9..027b0332fd1 100644
--- a/nixos/modules/services/audio/roon-bridge.nix
+++ b/nixos/modules/services/audio/roon-bridge.nix
@@ -42,7 +42,7 @@ in {
       environment.ROON_DATAROOT = "/var/lib/${name}";
 
       serviceConfig = {
-        ExecStart = "${pkgs.roon-bridge}/start.sh";
+        ExecStart = "${pkgs.roon-bridge}/bin/RoonBridge";
         LimitNOFILE = 8192;
         User = cfg.user;
         Group = cfg.group;
@@ -70,12 +70,11 @@ in {
 
     users.groups.${cfg.group} = {};
     users.users.${cfg.user} =
-      if cfg.user == "roon-bridge" then {
+      optionalAttrs (cfg.user == "roon-bridge") {
         isSystemUser = true;
         description = "Roon Bridge user";
         group = cfg.group;
         extraGroups = [ "audio" ];
-      }
-      else {};
+      };
   };
 }
diff --git a/nixos/modules/services/audio/roon-server.nix b/nixos/modules/services/audio/roon-server.nix
index fbe74f63b9d..8691c08b0d3 100644
--- a/nixos/modules/services/audio/roon-server.nix
+++ b/nixos/modules/services/audio/roon-server.nix
@@ -76,12 +76,11 @@ in {
 
     users.groups.${cfg.group} = {};
     users.users.${cfg.user} =
-      if cfg.user == "roon-server" then {
+      optionalAttrs (cfg.user == "roon-server") {
         isSystemUser = true;
         description = "Roon Server user";
         group = cfg.group;
         extraGroups = [ "audio" ];
-      }
-      else {};
+      };
   };
 }
diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix
index 2af42eeb370..dbab741bf6f 100644
--- a/nixos/modules/services/audio/snapserver.nix
+++ b/nixos/modules/services/audio/snapserver.nix
@@ -275,9 +275,9 @@ in {
 
     warnings =
       # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85
-      filter (w: w != "") (mapAttrsToList (k: v: if v.type == "spotify" then ''
+      filter (w: w != "") (mapAttrsToList (k: v: optionalString (v.type == "spotify") ''
         services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead.
-      '' else "") cfg.streams);
+      '') cfg.streams);
 
     systemd.services.snapserver = {
       after = [ "network.target" ];
diff --git a/nixos/modules/services/audio/tts.nix b/nixos/modules/services/audio/tts.nix
new file mode 100644
index 00000000000..1a355c8ee39
--- /dev/null
+++ b/nixos/modules/services/audio/tts.nix
@@ -0,0 +1,151 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.services.tts;
+in
+
+{
+  options.services.tts = let
+    inherit (lib) literalExpression mkOption mdDoc mkEnableOption types;
+  in  {
+    servers = mkOption {
+      type = types.attrsOf (types.submodule (
+        { ... }: {
+          options = {
+            enable = mkEnableOption (mdDoc "Coqui TTS server");
+
+            port = mkOption {
+              type = types.port;
+              example = 5000;
+              description = mdDoc ''
+                Port to bind the TTS server to.
+              '';
+            };
+
+            model = mkOption {
+              type = types.nullOr types.str;
+              default = "tts_models/en/ljspeech/tacotron2-DDC";
+              example = null;
+              description = mdDoc ''
+                Name of the model to download and use for speech synthesis.
+
+                Check `tts-server --list_models` for possible values.
+
+                Set to `null` to use a custom model.
+              '';
+            };
+
+            useCuda = mkOption {
+              type = types.bool;
+              default = false;
+              example = true;
+              description = mdDoc ''
+                Whether to offload computation onto a CUDA compatible GPU.
+              '';
+            };
+
+            extraArgs = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = mdDoc ''
+                Extra arguments to pass to the server commandline.
+              '';
+            };
+          };
+        }
+      ));
+      default = {};
+      example = literalExpression ''
+        {
+          english = {
+            port = 5300;
+            model = "tts_models/en/ljspeech/tacotron2-DDC";
+          };
+          german = {
+            port = 5301;
+            model = "tts_models/de/thorsten/tacotron2-DDC";
+          };
+          dutch = {
+            port = 5302;
+            model = "tts_models/nl/mai/tacotron2-DDC";
+          };
+        }
+      '';
+      description = mdDoc ''
+        TTS server instances.
+      '';
+    };
+  };
+
+  config = let
+    inherit (lib) mkIf mapAttrs' nameValuePair optionalString concatMapStringsSep escapeShellArgs;
+  in mkIf (cfg.servers != {}) {
+    systemd.services = mapAttrs' (server: options:
+      nameValuePair "tts-${server}" {
+        description = "Coqui TTS server instance ${server}";
+        after = [
+          "network-online.target"
+        ];
+        wantedBy = [
+          "multi-user.target"
+        ];
+        path = with pkgs; [
+          espeak-ng
+        ];
+        environment.HOME = "/var/lib/tts";
+        serviceConfig = {
+          DynamicUser = true;
+          User = "tts";
+          StateDirectory = "tts";
+          ExecStart = "${pkgs.tts}/bin/tts-server --port ${toString options.port}"
+            + optionalString (options.model != null) " --model_name ${options.model}"
+            + optionalString (options.useCuda) " --use_cuda"
+            + (concatMapStringsSep " " escapeShellArgs options.extraArgs);
+          CapabilityBoundingSet = "";
+          DeviceAllow = if options.useCuda then [
+            # https://docs.nvidia.com/dgx/pdf/dgx-os-5-user-guide.pdf
+            "/dev/nvidia1"
+            "/dev/nvidia2"
+            "/dev/nvidia3"
+            "/dev/nvidia4"
+            "/dev/nvidia-caps/nvidia-cap1"
+            "/dev/nvidia-caps/nvidia-cap2"
+            "/dev/nvidiactl"
+            "/dev/nvidia-modeset"
+            "/dev/nvidia-uvm"
+            "/dev/nvidia-uvm-tools"
+          ] else "";
+          DevicePolicy = "closed";
+          LockPersonality = true;
+          # jit via numba->llvmpipe
+          MemoryDenyWriteExecute = false;
+          PrivateDevices = true;
+          PrivateUsers = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectControlGroups = true;
+          ProtectProc = "invisible";
+          ProcSubset = "pid";
+          RestrictAddressFamilies = [
+            "AF_INET"
+            "AF_INET6"
+          ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [
+            "@system-service"
+            "~@privileged"
+          ];
+          UMask = "0077";
+        };
+      }) cfg.servers;
+  };
+}
diff --git a/nixos/modules/services/audio/wyoming/faster-whisper.nix b/nixos/modules/services/audio/wyoming/faster-whisper.nix
new file mode 100644
index 00000000000..6317709b247
--- /dev/null
+++ b/nixos/modules/services/audio/wyoming/faster-whisper.nix
@@ -0,0 +1,186 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.services.wyoming.faster-whisper;
+
+  inherit (lib)
+    escapeShellArgs
+    mkOption
+    mdDoc
+    mkEnableOption
+    mkPackageOptionMD
+    types
+    ;
+
+  inherit (builtins)
+    toString
+    ;
+
+in
+
+{
+  options.services.wyoming.faster-whisper = with types; {
+    package = mkPackageOptionMD pkgs "wyoming-faster-whisper" { };
+
+    servers = mkOption {
+      default = {};
+      description = mdDoc ''
+        Attribute set of faster-whisper instances to spawn.
+      '';
+      type = types.attrsOf (types.submodule (
+        { ... }: {
+          options = {
+            enable = mkEnableOption (mdDoc "Wyoming faster-whisper server");
+
+            model = mkOption {
+              type = enum [
+                "tiny"
+                "tiny-int8"
+                "base"
+                "base-int8"
+                "small"
+                "small-int8"
+                "medium"
+                "medium-int8"
+              ];
+              default = "tiny-int8";
+              example = "medium-int8";
+              description = mdDoc ''
+                Name of the voice model to use.
+              '';
+            };
+
+            uri = mkOption {
+              type = strMatching "^(tcp|unix)://.*$";
+              example = "tcp://0.0.0.0:10300";
+              description = mdDoc ''
+                URI to bind the wyoming server to.
+              '';
+            };
+
+            device = mkOption {
+              # https://opennmt.net/CTranslate2/python/ctranslate2.models.Whisper.html#
+              type = types.enum [
+                "cpu"
+                "cuda"
+                "auto"
+              ];
+              default = "cpu";
+              description = mdDoc ''
+                Id of a speaker in a multi-speaker model.
+              '';
+            };
+
+            language = mkOption {
+              type = enum [
+                # https://github.com/home-assistant/addons/blob/master/whisper/config.yaml#L20
+                "auto" "af" "am" "ar" "as" "az" "ba" "be" "bg" "bn" "bo" "br" "bs" "ca" "cs" "cy" "da" "de" "el" "en" "es" "et" "eu" "fa" "fi" "fo" "fr" "gl" "gu" "ha" "haw" "he" "hi" "hr" "ht" "hu" "hy" "id" "is" "it" "ja" "jw" "ka" "kk" "km" "kn" "ko" "la" "lb" "ln" "lo" "lt" "lv" "mg" "mi" "mk" "ml" "mn" "mr" "ms" "mt" "my" "ne" "nl" "nn" "no" "oc" "pa" "pl" "ps" "pt" "ro" "ru" "sa" "sd" "si" "sk" "sl" "sn" "so" "sq" "sr" "su" "sv" "sw" "ta" "te" "tg" "th" "tk" "tl" "tr" "tt" "uk" "ur" "uz" "vi" "yi" "yo" "zh"
+              ];
+              example = "en";
+              description = mdDoc ''
+                The language used to to parse words and sentences.
+              '';
+            };
+
+            beamSize = mkOption {
+              type = ints.unsigned;
+              default = 1;
+              example = 5;
+              description = mdDoc ''
+                The number of beams to use in beam search.
+              '';
+              apply = toString;
+            };
+
+            extraArgs = mkOption {
+              type = listOf str;
+              default = [ ];
+              description = mdDoc ''
+                Extra arguments to pass to the server commandline.
+              '';
+              apply = escapeShellArgs;
+            };
+          };
+        }
+      ));
+    };
+  };
+
+  config = let
+    inherit (lib)
+      mapAttrs'
+      mkIf
+      nameValuePair
+    ;
+  in mkIf (cfg.servers != {}) {
+    systemd.services = mapAttrs' (server: options:
+      nameValuePair "wyoming-faster-whisper-${server}" {
+        description = "Wyoming faster-whisper server instance ${server}";
+        after = [
+          "network-online.target"
+        ];
+        wantedBy = [
+          "multi-user.target"
+        ];
+        serviceConfig = {
+          DynamicUser = true;
+          User = "wyoming-faster-whisper";
+          StateDirectory = "wyoming/faster-whisper";
+          # https://github.com/home-assistant/addons/blob/master/whisper/rootfs/etc/s6-overlay/s6-rc.d/whisper/run
+          ExecStart = ''
+            ${cfg.package}/bin/wyoming-faster-whisper \
+              --data-dir $STATE_DIRECTORY \
+              --download-dir $STATE_DIRECTORY \
+              --uri ${options.uri} \
+              --model ${options.model} \
+              --language ${options.language} \
+              --beam-size ${options.beamSize} ${options.extraArgs}
+          '';
+          CapabilityBoundingSet = "";
+          DeviceAllow = if builtins.elem options.device [ "cuda" "auto" ] then [
+            # https://docs.nvidia.com/dgx/pdf/dgx-os-5-user-guide.pdf
+            "/dev/nvidia1"
+            "/dev/nvidia2"
+            "/dev/nvidia3"
+            "/dev/nvidia4"
+            "/dev/nvidia-caps/nvidia-cap1"
+            "/dev/nvidia-caps/nvidia-cap2"
+            "/dev/nvidiactl"
+            "/dev/nvidia-modeset"
+            "/dev/nvidia-uvm"
+            "/dev/nvidia-uvm-tools"
+          ] else "";
+          DevicePolicy = "closed";
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          PrivateDevices = true;
+          PrivateUsers = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectControlGroups = true;
+          ProtectProc = "invisible";
+          ProcSubset = "pid";
+          RestrictAddressFamilies = [
+            "AF_INET"
+            "AF_INET6"
+            "AF_UNIX"
+          ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [
+            "@system-service"
+            "~@privileged"
+          ];
+          UMask = "0077";
+        };
+      }) cfg.servers;
+  };
+}
diff --git a/nixos/modules/services/audio/wyoming/piper.nix b/nixos/modules/services/audio/wyoming/piper.nix
new file mode 100644
index 00000000000..ed50bd9f48e
--- /dev/null
+++ b/nixos/modules/services/audio/wyoming/piper.nix
@@ -0,0 +1,174 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.services.wyoming.piper;
+
+  inherit (lib)
+    escapeShellArgs
+    mkOption
+    mdDoc
+    mkEnableOption
+    mkPackageOptionMD
+    types
+    ;
+
+  inherit (builtins)
+    toString
+    ;
+
+in
+
+{
+  meta.buildDocsInSandbox = false;
+
+  options.services.wyoming.piper = with types; {
+    package = mkPackageOptionMD pkgs "wyoming-piper" { };
+
+    servers = mkOption {
+      default = {};
+      description = mdDoc ''
+        Attribute set of piper instances to spawn.
+      '';
+      type = types.attrsOf (types.submodule (
+        { ... }: {
+          options = {
+            enable = mkEnableOption (mdDoc "Wyoming Piper server");
+
+            piper = mkPackageOptionMD pkgs "piper-tts" { };
+
+            voice = mkOption {
+              type = str;
+              example = "en-us-ryan-medium";
+              description = mdDoc ''
+                Name of the voice model to use. See the following website for samples:
+                https://rhasspy.github.io/piper-samples/
+              '';
+            };
+
+            uri = mkOption {
+              type = strMatching "^(tcp|unix)://.*$";
+              example = "tcp://0.0.0.0:10200";
+              description = mdDoc ''
+                URI to bind the wyoming server to.
+              '';
+            };
+
+            speaker = mkOption {
+              type = ints.unsigned;
+              default = 0;
+              description = mdDoc ''
+                ID of a specific speaker in a multi-speaker model.
+              '';
+              apply = toString;
+            };
+
+            noiseScale = mkOption {
+              type = float;
+              default = 0.667;
+              description = mdDoc ''
+                Generator noise value.
+              '';
+              apply = toString;
+            };
+
+            noiseWidth = mkOption {
+              type = float;
+              default = 0.333;
+              description = mdDoc ''
+                Phoneme width noise value.
+              '';
+              apply = toString;
+            };
+
+            lengthScale = mkOption {
+              type = float;
+              default = 1.0;
+              description = mdDoc ''
+                Phoneme length value.
+              '';
+              apply = toString;
+            };
+
+            extraArgs = mkOption {
+              type = listOf str;
+              default = [ ];
+              description = mdDoc ''
+                Extra arguments to pass to the server commandline.
+              '';
+              apply = escapeShellArgs;
+            };
+          };
+        }
+      ));
+    };
+  };
+
+  config = let
+    inherit (lib)
+      mapAttrs'
+      mkIf
+      nameValuePair
+    ;
+  in mkIf (cfg.servers != {}) {
+    systemd.services = mapAttrs' (server: options:
+      nameValuePair "wyoming-piper-${server}" {
+        description = "Wyoming Piper server instance ${server}";
+        after = [
+          "network-online.target"
+        ];
+        wantedBy = [
+          "multi-user.target"
+        ];
+        serviceConfig = {
+          DynamicUser = true;
+          User = "wyoming-piper";
+          StateDirectory = "wyoming/piper";
+          # https://github.com/home-assistant/addons/blob/master/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/run
+          ExecStart = ''
+            ${cfg.package}/bin/wyoming-piper \
+              --data-dir $STATE_DIRECTORY \
+              --download-dir $STATE_DIRECTORY \
+              --uri ${options.uri} \
+              --piper ${options.piper}/bin/piper \
+              --voice ${options.voice} \
+              --speaker ${options.speaker} \
+              --length-scale ${options.lengthScale} \
+              --noise-scale ${options.noiseScale} \
+              --noise-w ${options.noiseWidth} ${options.extraArgs}
+          '';
+          CapabilityBoundingSet = "";
+          DeviceAllow = "";
+          DevicePolicy = "closed";
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          PrivateDevices = true;
+          PrivateUsers = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectControlGroups = true;
+          ProtectProc = "invisible";
+          ProcSubset = "pid";
+          RestrictAddressFamilies = [
+            "AF_INET"
+            "AF_INET6"
+            "AF_UNIX"
+          ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [
+            "@system-service"
+            "~@privileged"
+          ];
+          UMask = "0077";
+        };
+      }) cfg.servers;
+  };
+}
diff --git a/nixos/modules/services/audio/ympd.nix b/nixos/modules/services/audio/ympd.nix
index 811b81030ef..b74cc3f9c0b 100644
--- a/nixos/modules/services/audio/ympd.nix
+++ b/nixos/modules/services/audio/ympd.nix
@@ -48,8 +48,46 @@ in {
 
     systemd.services.ympd = {
       description = "Standalone MPD Web GUI written in C";
+
       wantedBy = [ "multi-user.target" ];
-      serviceConfig.ExecStart = "${pkgs.ympd}/bin/ympd --host ${cfg.mpd.host} --port ${toString cfg.mpd.port} --webport ${toString cfg.webPort} --user nobody";
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.ympd}/bin/ympd \
+            --host ${cfg.mpd.host} \
+            --port ${toString cfg.mpd.port} \
+            --webport ${toString cfg.webPort}
+        '';
+
+        DynamicUser = true;
+        NoNewPrivileges = true;
+
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        ProtectHome = "tmpfs";
+
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateIPC = true;
+
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        SystemCallFilter = [
+          "@system-service"
+          "~@process"
+          "~@setuid"
+        ];
+      };
     };
 
   };
diff --git a/nixos/modules/services/backup/automysqlbackup.nix b/nixos/modules/services/backup/automysqlbackup.nix
index d0237f196a8..27bbff813b1 100644
--- a/nixos/modules/services/backup/automysqlbackup.nix
+++ b/nixos/modules/services/backup/automysqlbackup.nix
@@ -3,7 +3,7 @@
 let
 
   inherit (lib) concatMapStringsSep concatStringsSep isInt isList literalExpression;
-  inherit (lib) mapAttrs mapAttrsToList mkDefault mkEnableOption mkIf mkOption optional types;
+  inherit (lib) mapAttrs mapAttrsToList mkDefault mkEnableOption mkIf mkOption mkRenamedOptionModule optional types;
 
   cfg = config.services.automysqlbackup;
   pkg = pkgs.automysqlbackup;
@@ -26,6 +26,10 @@ let
 
 in
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "automysqlbackup" "config" ] [ "services" "automysqlbackup" "settings" ])
+  ];
+
   # interface
   options = {
     services.automysqlbackup = {
@@ -40,7 +44,7 @@ in
         '';
       };
 
-      config = mkOption {
+      settings = mkOption {
         type = with types; attrsOf (oneOf [ str int bool (listOf str) ]);
         default = {};
         description = lib.mdDoc ''
@@ -112,7 +116,18 @@ in
 
     services.mysql.ensureUsers = optional (config.services.mysql.enable && cfg.config.mysql_dump_host == "localhost") {
       name = user;
-      ensurePermissions = { "*.*" = "SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT"; };
+      ensurePermissions = {
+        "*.*" = "SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT";
+
+        # https://forums.mysql.com/read.php?10,668311,668315#msg-668315
+        "function sys.extract_table_from_file_name" = "execute";
+        "function sys.format_path" = "execute";
+        "function sys.format_statement" = "execute";
+        "function sys.extract_schema_from_file_name" = "execute";
+        "function sys.ps_thread_account" = "execute";
+        "function sys.format_time" = "execute";
+        "function sys.format_bytes" = "execute";
+      };
     };
 
   };
diff --git a/nixos/modules/services/backup/borgbackup.md b/nixos/modules/services/backup/borgbackup.md
new file mode 100644
index 00000000000..39141f6ec85
--- /dev/null
+++ b/nixos/modules/services/backup/borgbackup.md
@@ -0,0 +1,163 @@
+# BorgBackup {#module-borgbase}
+
+*Source:* {file}`modules/services/backup/borgbackup.nix`
+
+*Upstream documentation:* <https://borgbackup.readthedocs.io/>
+
+[BorgBackup](https://www.borgbackup.org/) (short: Borg)
+is a deduplicating backup program. Optionally, it supports compression and
+authenticated encryption.
+
+The main goal of Borg is to provide an efficient and secure way to backup
+data. The data deduplication technique used makes Borg suitable for daily
+backups since only changes are stored. The authenticated encryption technique
+makes it suitable for backups to not fully trusted targets.
+
+## Configuring {#module-services-backup-borgbackup-configuring}
+
+A complete list of options for the Borgbase module may be found
+[here](#opt-services.borgbackup.jobs).
+
+## Basic usage for a local backup {#opt-services-backup-borgbackup-local-directory}
+
+A very basic configuration for backing up to a locally accessible directory is:
+```
+{
+    opt.services.borgbackup.jobs = {
+      { rootBackup = {
+          paths = "/";
+          exclude = [ "/nix" "/path/to/local/repo" ];
+          repo = "/path/to/local/repo";
+          doInit = true;
+          encryption = {
+            mode = "repokey";
+            passphrase = "secret";
+          };
+          compression = "auto,lzma";
+          startAt = "weekly";
+        };
+      }
+    };
+}
+```
+
+::: {.warning}
+If you do not want the passphrase to be stored in the world-readable
+Nix store, use passCommand. You find an example below.
+:::
+
+## Create a borg backup server {#opt-services-backup-create-server}
+
+You should use a different SSH key for each repository you write to,
+because the specified keys are restricted to running borg serve and can only
+access this single repository. You need the output of the generate pub file.
+
+```ShellSession
+# sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_my_borg_repo
+# cat /run/keys/id_ed25519_my_borg_repo
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos
+```
+
+Add the following snippet to your NixOS configuration:
+```
+{
+  services.borgbackup.repos = {
+    my_borg_repo = {
+      authorizedKeys = [
+        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos"
+      ] ;
+      path = "/var/lib/my_borg_repo" ;
+    };
+  };
+}
+```
+
+## Backup to the borg repository server {#opt-services-backup-borgbackup-remote-server}
+
+The following NixOS snippet creates an hourly backup to the service
+(on the host nixos) as created in the section above. We assume
+that you have stored a secret passphrasse in the file
+{file}`/run/keys/borgbackup_passphrase`, which should be only
+accessible by root
+
+```
+{
+  services.borgbackup.jobs = {
+    backupToLocalServer = {
+      paths = [ "/etc/nixos" ];
+      doInit = true;
+      repo =  "borg@nixos:." ;
+      encryption = {
+        mode = "repokey-blake2";
+        passCommand = "cat /run/keys/borgbackup_passphrase";
+      };
+      environment = { BORG_RSH = "ssh -i /run/keys/id_ed25519_my_borg_repo"; };
+      compression = "auto,lzma";
+      startAt = "hourly";
+    };
+  };
+};
+```
+
+The following few commands (run as root) let you test your backup.
+```
+> nixos-rebuild switch
+...restarting the following units: polkit.service
+> systemctl restart borgbackup-job-backupToLocalServer
+> sleep 10
+> systemctl restart borgbackup-job-backupToLocalServer
+> export BORG_PASSPHRASE=topSecrect
+> borg list --rsh='ssh -i /run/keys/id_ed25519_my_borg_repo' borg@nixos:.
+nixos-backupToLocalServer-2020-03-30T21:46:17 Mon, 2020-03-30 21:46:19 [84feb97710954931ca384182f5f3cb90665f35cef214760abd7350fb064786ac]
+nixos-backupToLocalServer-2020-03-30T21:46:30 Mon, 2020-03-30 21:46:32 [e77321694ecd160ca2228611747c6ad1be177d6e0d894538898de7a2621b6e68]
+```
+
+## Backup to a hosting service {#opt-services-backup-borgbackup-borgbase}
+
+Several companies offer [(paid) hosting services](https://www.borgbackup.org/support/commercial.html)
+for Borg repositories.
+
+To backup your home directory to borgbase you have to:
+
+  - Generate a SSH key without a password, to access the remote server. E.g.
+
+        sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_borgbase
+
+  - Create the repository on the server by following the instructions for your
+    hosting server.
+  - Initialize the repository on the server. Eg.
+
+        sudo borg init --encryption=repokey-blake2  \
+            --rsh "ssh -i /run/keys/id_ed25519_borgbase" \
+            zzz2aaaaa@zzz2aaaaa.repo.borgbase.com:repo
+
+  - Add it to your NixOS configuration, e.g.
+
+        {
+            services.borgbackup.jobs = {
+            my_Remote_Backup = {
+                paths = [ "/" ];
+                exclude = [ "/nix" "'**/.cache'" ];
+                repo =  "zzz2aaaaa@zzz2aaaaa.repo.borgbase.com:repo";
+                  encryption = {
+                  mode = "repokey-blake2";
+                  passCommand = "cat /run/keys/borgbackup_passphrase";
+                };
+                environment = { BORG_RSH = "ssh -i /run/keys/id_ed25519_borgbase"; };
+                compression = "auto,lzma";
+                startAt = "daily";
+            };
+          };
+        }}
+
+## Vorta backup client for the desktop {#opt-services-backup-borgbackup-vorta}
+
+Vorta is a backup client for macOS and Linux desktops. It integrates the
+mighty BorgBackup with your desktop environment to protect your data from
+disk failure, ransomware and theft.
+
+It can be installed in NixOS e.g. by adding `pkgs.vorta`
+to [](#opt-environment.systemPackages).
+
+Details about using Vorta can be found under
+[https://vorta.borgbase.com](https://vorta.borgbase.com/usage) .
diff --git a/nixos/modules/services/backup/borgbackup.nix b/nixos/modules/services/backup/borgbackup.nix
index c5fc09dcea0..0da70112d48 100644
--- a/nixos/modules/services/backup/borgbackup.nix
+++ b/nixos/modules/services/backup/borgbackup.nix
@@ -66,6 +66,7 @@ let
       ${mkKeepArgs cfg} \
       ${optionalString (cfg.prune.prefix != null) "--glob-archives ${escapeShellArg "${cfg.prune.prefix}*"}"} \
       $extraPruneArgs
+    borg compact $extraArgs $extraCompactArgs
     ${cfg.postPrune}
   '');
 
@@ -108,7 +109,7 @@ let
       };
       environment = {
         BORG_REPO = cfg.repo;
-        inherit (cfg) extraArgs extraInitArgs extraCreateArgs extraPruneArgs;
+        inherit (cfg) extraArgs extraInitArgs extraCreateArgs extraPruneArgs extraCompactArgs;
       } // (mkPassEnv cfg) // cfg.environment;
     };
 
@@ -226,7 +227,7 @@ let
 
 in {
   meta.maintainers = with maintainers; [ dotlambda ];
-  meta.doc = ./borgbackup.xml;
+  meta.doc = ./borgbackup.md;
 
   ###### interface
 
@@ -638,6 +639,15 @@ in {
             example = "--save-space";
           };
 
+          extraCompactArgs = mkOption {
+            type = types.str;
+            description = lib.mdDoc ''
+              Additional arguments for {command}`borg compact`.
+              Can also be set at runtime using `$extraCompactArgs`.
+            '';
+            default = "";
+            example = "--cleanup-commits";
+          };
         };
       }
     ));
diff --git a/nixos/modules/services/backup/borgbackup.xml b/nixos/modules/services/backup/borgbackup.xml
deleted file mode 100644
index f38064f8677..00000000000
--- a/nixos/modules/services/backup/borgbackup.xml
+++ /dev/null
@@ -1,209 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-borgbase">
- <title>BorgBackup</title>
-  <para>
-  <emphasis>Source:</emphasis>
-  <filename>modules/services/backup/borgbackup.nix</filename>
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="https://borgbackup.readthedocs.io/"/>
- </para>
- <para>
-  <link xlink:href="https://www.borgbackup.org/">BorgBackup</link> (short: Borg)
-  is a deduplicating backup program. Optionally, it supports compression and
-  authenticated encryption.
-  </para>
-  <para>
-  The main goal of Borg is to provide an efficient and secure way to backup
-  data. The data deduplication technique used makes Borg suitable for daily
-  backups since only changes are stored. The authenticated encryption technique
-  makes it suitable for backups to not fully trusted targets.
- </para>
-  <section xml:id="module-services-backup-borgbackup-configuring">
-  <title>Configuring</title>
-  <para>
-   A complete list of options for the Borgbase module may be found
-   <link linkend="opt-services.borgbackup.jobs">here</link>.
-  </para>
-</section>
- <section xml:id="opt-services-backup-borgbackup-local-directory">
-  <title>Basic usage for a local backup</title>
-
-  <para>
-   A very basic configuration for backing up to a locally accessible directory
-   is:
-<programlisting>
-{
-    opt.services.borgbackup.jobs = {
-      { rootBackup = {
-          paths = "/";
-          exclude = [ "/nix" "/path/to/local/repo" ];
-          repo = "/path/to/local/repo";
-          doInit = true;
-          encryption = {
-            mode = "repokey";
-            passphrase = "secret";
-          };
-          compression = "auto,lzma";
-          startAt = "weekly";
-        };
-      }
-    };
-}</programlisting>
-  </para>
-  <warning>
-    <para>
-        If you do not want the passphrase to be stored in the world-readable
-        Nix store, use passCommand. You find an example below.
-    </para>
-  </warning>
- </section>
-<section xml:id="opt-services-backup-create-server">
-  <title>Create a borg backup server</title>
-  <para>You should use a different SSH key for each repository you write to,
-    because the specified keys are restricted to running borg serve and can only
-    access this single repository. You need the output of the generate pub file.
-  </para>
-    <para>
-<screen>
-<prompt># </prompt>sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_my_borg_repo
-<prompt># </prompt>cat /run/keys/id_ed25519_my_borg_repo
-ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos</screen>
-    </para>
-    <para>
-      Add the following snippet to your NixOS configuration:
-      <programlisting>
-{
-  services.borgbackup.repos = {
-    my_borg_repo = {
-      authorizedKeys = [
-        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos"
-      ] ;
-      path = "/var/lib/my_borg_repo" ;
-    };
-  };
-}</programlisting>
-    </para>
-</section>
-
- <section xml:id="opt-services-backup-borgbackup-remote-server">
-  <title>Backup to the borg repository server</title>
-  <para>The following NixOS snippet creates an hourly backup to the service
-    (on the host nixos) as created in the section above. We assume
-    that you have stored a secret passphrasse in the file
-    <code>/run/keys/borgbackup_passphrase</code>, which should be only
-    accessible by root
-  </para>
-  <para>
-      <programlisting>
-{
-  services.borgbackup.jobs = {
-    backupToLocalServer = {
-      paths = [ "/etc/nixos" ];
-      doInit = true;
-      repo =  "borg@nixos:." ;
-      encryption = {
-        mode = "repokey-blake2";
-        passCommand = "cat /run/keys/borgbackup_passphrase";
-      };
-      environment = { BORG_RSH = "ssh -i /run/keys/id_ed25519_my_borg_repo"; };
-      compression = "auto,lzma";
-      startAt = "hourly";
-    };
-  };
-};</programlisting>
-  </para>
-  <para>The following few commands (run as root) let you test your backup.
-      <programlisting>
-> nixos-rebuild switch
-...restarting the following units: polkit.service
-> systemctl restart borgbackup-job-backupToLocalServer
-> sleep 10
-> systemctl restart borgbackup-job-backupToLocalServer
-> export BORG_PASSPHRASE=topSecrect
-> borg list --rsh='ssh -i /run/keys/id_ed25519_my_borg_repo' borg@nixos:.
-nixos-backupToLocalServer-2020-03-30T21:46:17 Mon, 2020-03-30 21:46:19 [84feb97710954931ca384182f5f3cb90665f35cef214760abd7350fb064786ac]
-nixos-backupToLocalServer-2020-03-30T21:46:30 Mon, 2020-03-30 21:46:32 [e77321694ecd160ca2228611747c6ad1be177d6e0d894538898de7a2621b6e68]</programlisting>
-    </para>
-</section>
-
- <section xml:id="opt-services-backup-borgbackup-borgbase">
-  <title>Backup to a hosting service</title>
-
-  <para>
-    Several companies offer <link
-      xlink:href="https://www.borgbackup.org/support/commercial.html">(paid)
-      hosting services</link> for Borg repositories.
-  </para>
-  <para>
-    To backup your home directory to borgbase you have to:
-  </para>
-  <itemizedlist>
-  <listitem>
-    <para>
-      Generate a SSH key without a password, to access the remote server. E.g.
-    </para>
-    <para>
-        <programlisting>sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_borgbase</programlisting>
-    </para>
-  </listitem>
-  <listitem>
-    <para>
-      Create the repository on the server by following the instructions for your
-      hosting server.
-    </para>
-  </listitem>
-  <listitem>
-    <para>
-      Initialize the repository on the server. Eg.
-      <programlisting>
-sudo borg init --encryption=repokey-blake2  \
-    -rsh "ssh -i /run/keys/id_ed25519_borgbase" \
-    zzz2aaaaa@zzz2aaaaa.repo.borgbase.com:repo</programlisting>
-  </para>
-  </listitem>
-  <listitem>
-<para>Add it to your NixOS configuration, e.g.
-<programlisting>
-{
-    services.borgbackup.jobs = {
-    my_Remote_Backup = {
-        paths = [ "/" ];
-        exclude = [ "/nix" "'**/.cache'" ];
-        repo =  "zzz2aaaaa@zzz2aaaaa.repo.borgbase.com:repo";
-          encryption = {
-          mode = "repokey-blake2";
-          passCommand = "cat /run/keys/borgbackup_passphrase";
-        };
-        environment = { BORG_RSH = "ssh -i /run/keys/id_ed25519_borgbase"; };
-        compression = "auto,lzma";
-        startAt = "daily";
-    };
-  };
-}}</programlisting>
-  </para>
-  </listitem>
-</itemizedlist>
- </section>
-  <section xml:id="opt-services-backup-borgbackup-vorta">
-  <title>Vorta backup client for the desktop</title>
-  <para>
-    Vorta is a backup client for macOS and Linux desktops. It integrates the
-    mighty BorgBackup with your desktop environment to protect your data from
-    disk failure, ransomware and theft.
-  </para>
-  <para>
-   It can be installed in NixOS e.g. by adding <package>pkgs.vorta</package>
-   to <xref linkend="opt-environment.systemPackages" />.
-  </para>
-  <para>
-    Details about using Vorta can be found under <link
-      xlink:href="https://vorta.borgbase.com/usage">https://vorta.borgbase.com
-      </link>.
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/backup/borgmatic.nix b/nixos/modules/services/backup/borgmatic.nix
index 73c4acda393..5ee036e68c7 100644
--- a/nixos/modules/services/backup/borgmatic.nix
+++ b/nixos/modules/services/backup/borgmatic.nix
@@ -5,44 +5,58 @@ with lib;
 let
   cfg = config.services.borgmatic;
   settingsFormat = pkgs.formats.yaml { };
+
+  cfgType = with types; submodule {
+    freeformType = settingsFormat.type;
+    options.location = {
+      source_directories = mkOption {
+        type = listOf str;
+        description = mdDoc ''
+          List of source directories to backup (required). Globs and
+          tildes are expanded.
+        '';
+        example = [ "/home" "/etc" "/var/log/syslog*" ];
+      };
+      repositories = mkOption {
+        type = listOf str;
+        description = mdDoc ''
+          Paths to local or remote repositories (required). Tildes are
+          expanded. Multiple repositories are backed up to in
+          sequence. Borg placeholders can be used. See the output of
+          "borg help placeholders" for details. See ssh_command for
+          SSH options like identity file or port. If systemd service
+          is used, then add local repository paths in the systemd
+          service file to the ReadWritePaths list.
+        '';
+        example = [
+          "ssh://user@backupserver/./sourcehostname.borg"
+          "ssh://user@backupserver/./{fqdn}"
+          "/var/local/backups/local.borg"
+        ];
+      };
+    };
+  };
+
   cfgfile = settingsFormat.generate "config.yaml" cfg.settings;
-in {
+in
+{
   options.services.borgmatic = {
-    enable = mkEnableOption (lib.mdDoc "borgmatic");
+    enable = mkEnableOption (mdDoc "borgmatic");
 
     settings = mkOption {
-      description = lib.mdDoc ''
+      description = mdDoc ''
         See https://torsion.org/borgmatic/docs/reference/configuration/
       '';
-      type = types.submodule {
-        freeformType = settingsFormat.type;
-        options.location = {
-          source_directories = mkOption {
-            type = types.listOf types.str;
-            description = lib.mdDoc ''
-              List of source directories to backup (required). Globs and
-              tildes are expanded.
-            '';
-            example = [ "/home" "/etc" "/var/log/syslog*" ];
-          };
-          repositories = mkOption {
-            type = types.listOf types.str;
-            description = lib.mdDoc ''
-              Paths to local or remote repositories (required). Tildes are
-              expanded. Multiple repositories are backed up to in
-              sequence. Borg placeholders can be used. See the output of
-              "borg help placeholders" for details. See ssh_command for
-              SSH options like identity file or port. If systemd service
-              is used, then add local repository paths in the systemd
-              service file to the ReadWritePaths list.
-            '';
-            example = [
-              "user@backupserver:sourcehostname.borg"
-              "user@backupserver:{fqdn}"
-            ];
-          };
-        };
-      };
+      default = null;
+      type = types.nullOr cfgType;
+    };
+
+    configurations = mkOption {
+      description = mdDoc ''
+        Set of borgmatic configurations, see https://torsion.org/borgmatic/docs/reference/configuration/
+      '';
+      default = { };
+      type = types.attrsOf cfgType;
     };
   };
 
@@ -50,9 +64,16 @@ in {
 
     environment.systemPackages = [ pkgs.borgmatic ];
 
-    environment.etc."borgmatic/config.yaml".source = cfgfile;
+    environment.etc = (optionalAttrs (cfg.settings != null) { "borgmatic/config.yaml".source = cfgfile; }) //
+      mapAttrs'
+        (name: value: nameValuePair
+          "borgmatic.d/${name}.yaml"
+          { source = settingsFormat.generate "${name}.yaml" value; })
+        cfg.configurations;
 
     systemd.packages = [ pkgs.borgmatic ];
 
+    # Workaround: https://github.com/NixOS/nixpkgs/issues/81138
+    systemd.timers.borgmatic.wantedBy = [ "timers.target" ];
   };
 }
diff --git a/nixos/modules/services/backup/btrbk.nix b/nixos/modules/services/backup/btrbk.nix
index b6eb68cc43f..b838c174553 100644
--- a/nixos/modules/services/backup/btrbk.nix
+++ b/nixos/modules/services/backup/btrbk.nix
@@ -47,7 +47,12 @@ let
     then [ "${name} ${value}" ]
     else concatLists (mapAttrsToList (genSection name) value);
 
-  addDefaults = settings: { backend = "btrfs-progs-sudo"; } // settings;
+  sudo_doas =
+    if config.security.sudo.enable then "sudo"
+    else if config.security.doas.enable then "doas"
+    else throw "The btrbk nixos module needs either sudo or doas enabled in the configuration";
+
+  addDefaults = settings: { backend = "btrfs-progs-${sudo_doas}"; } // settings;
 
   mkConfigFile = name: settings: pkgs.writeTextFile {
     name = "btrbk-${name}.conf";
@@ -152,20 +157,41 @@ in
   };
   config = mkIf (sshEnabled || serviceEnabled) {
     environment.systemPackages = [ pkgs.btrbk ] ++ cfg.extraPackages;
-    security.sudo.extraRules = [
-      {
-        users = [ "btrbk" ];
-        commands = [
-          { command = "${pkgs.btrfs-progs}/bin/btrfs"; options = [ "NOPASSWD" ]; }
-          { command = "${pkgs.coreutils}/bin/mkdir"; options = [ "NOPASSWD" ]; }
-          { command = "${pkgs.coreutils}/bin/readlink"; options = [ "NOPASSWD" ]; }
-          # for ssh, they are not the same than the one hard coded in ${pkgs.btrbk}
-          { command = "/run/current-system/bin/btrfs"; options = [ "NOPASSWD" ]; }
-          { command = "/run/current-system/sw/bin/mkdir"; options = [ "NOPASSWD" ]; }
-          { command = "/run/current-system/sw/bin/readlink"; options = [ "NOPASSWD" ]; }
+    security.sudo = mkIf (sudo_doas == "sudo") {
+      extraRules = [
+        {
+            users = [ "btrbk" ];
+            commands = [
+            { command = "${pkgs.btrfs-progs}/bin/btrfs"; options = [ "NOPASSWD" ]; }
+            { command = "${pkgs.coreutils}/bin/mkdir"; options = [ "NOPASSWD" ]; }
+            { command = "${pkgs.coreutils}/bin/readlink"; options = [ "NOPASSWD" ]; }
+            # for ssh, they are not the same than the one hard coded in ${pkgs.btrbk}
+            { command = "/run/current-system/bin/btrfs"; options = [ "NOPASSWD" ]; }
+            { command = "/run/current-system/sw/bin/mkdir"; options = [ "NOPASSWD" ]; }
+            { command = "/run/current-system/sw/bin/readlink"; options = [ "NOPASSWD" ]; }
+            ];
+        }
+      ];
+    };
+    security.doas = mkIf (sudo_doas == "doas") {
+      extraRules = let
+        doasCmdNoPass = cmd: { users = [ "btrbk" ]; cmd = cmd; noPass = true; };
+      in
+        [
+            (doasCmdNoPass "${pkgs.btrfs-progs}/bin/btrfs")
+            (doasCmdNoPass "${pkgs.coreutils}/bin/mkdir")
+            (doasCmdNoPass "${pkgs.coreutils}/bin/readlink")
+            # for ssh, they are not the same than the one hard coded in ${pkgs.btrbk}
+            (doasCmdNoPass "/run/current-system/bin/btrfs")
+            (doasCmdNoPass "/run/current-system/sw/bin/mkdir")
+            (doasCmdNoPass "/run/current-system/sw/bin/readlink")
+
+            # doas matches command, not binary
+            (doasCmdNoPass "btrfs")
+            (doasCmdNoPass "mkdir")
+            (doasCmdNoPass "readlink")
         ];
-      }
-    ];
+    };
     users.users.btrbk = {
       isSystemUser = true;
       # ssh needs a home directory
@@ -183,8 +209,9 @@ in
               "best-effort" = 2;
               "realtime" = 1;
             }.${cfg.ioSchedulingClass};
+            sudo_doas_flag = "--${sudo_doas}";
           in
-          ''command="${pkgs.util-linux}/bin/ionice -t -c ${toString ioniceClass} ${optionalString (cfg.niceness >= 1) "${pkgs.coreutils}/bin/nice -n ${toString cfg.niceness}"} ${pkgs.btrbk}/share/btrbk/scripts/ssh_filter_btrbk.sh --sudo ${options}" ${v.key}''
+          ''command="${pkgs.util-linux}/bin/ionice -t -c ${toString ioniceClass} ${optionalString (cfg.niceness >= 1) "${pkgs.coreutils}/bin/nice -n ${toString cfg.niceness}"} ${pkgs.btrbk}/share/btrbk/scripts/ssh_filter_btrbk.sh ${sudo_doas_flag} ${options}" ${v.key}''
         )
         cfg.sshAccess;
     };
diff --git a/nixos/modules/services/backup/mysql-backup.nix b/nixos/modules/services/backup/mysql-backup.nix
index 289291c6bd2..9fbc599cd41 100644
--- a/nixos/modules/services/backup/mysql-backup.nix
+++ b/nixos/modules/services/backup/mysql-backup.nix
@@ -20,7 +20,7 @@ let
   '';
   backupDatabaseScript = db: ''
     dest="${cfg.location}/${db}.gz"
-    if ${mariadb}/bin/mysqldump ${if cfg.singleTransaction then "--single-transaction" else ""} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then
+    if ${mariadb}/bin/mysqldump ${optionalString cfg.singleTransaction "--single-transaction"} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then
       mv $dest.tmp $dest
       echo "Backed up to $dest"
     else
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
index 869ed5d9976..1620770e5b5 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -126,17 +126,34 @@ in
           ];
         };
 
+        exclude = mkOption {
+          type = types.listOf types.str;
+          default = [ ];
+          description = lib.mdDoc ''
+            Patterns to exclude when backing up. See
+            https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for
+            details on syntax.
+          '';
+          example = [
+            "/var/cache"
+            "/home/*/.cache"
+            ".git"
+          ];
+        };
+
         timerConfig = mkOption {
           type = types.attrsOf unitOption;
           default = {
             OnCalendar = "daily";
+            Persistent = true;
           };
           description = lib.mdDoc ''
-            When to run the backup. See man systemd.timer for details.
+            When to run the backup. See {manpage}`systemd.timer(5)` for details.
           '';
           example = {
             OnCalendar = "00:05";
             RandomizedDelaySec = "5h";
+            Persistent = true;
           };
         };
 
@@ -249,6 +266,7 @@ in
     example = {
       localbackup = {
         paths = [ "/home" ];
+        exclude = [ "/home/*/.cache" ];
         repository = "/mnt/backup-hdd";
         passwordFile = "/etc/nixos/secrets/restic-password";
         initialize = true;
@@ -270,20 +288,25 @@ in
 
   config = {
     warnings = mapAttrsToList (n: v: "services.restic.backups.${n}.s3CredentialsFile is deprecated, please use services.restic.backups.${n}.environmentFile instead.") (filterAttrs (n: v: v.s3CredentialsFile != null) config.services.restic.backups);
+    assertions = mapAttrsToList (n: v: {
+      assertion = (v.repository == null) != (v.repositoryFile == null);
+      message = "services.restic.backups.${n}: exactly one of repository or repositoryFile should be set";
+    }) config.services.restic.backups;
     systemd.services =
       mapAttrs'
         (name: backup:
           let
             extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
             resticCmd = "${backup.package}/bin/restic${extraOptions}";
+            excludeFlags = optional (backup.exclude != []) "--exclude-file=${pkgs.writeText "exclude-patterns" (concatStringsSep "\n" backup.exclude)}";
             filesFromTmpFile = "/run/restic-backups-${name}/includes";
             backupPaths =
               if (backup.dynamicFilesFrom == null)
-              then if (backup.paths != null) then concatStringsSep " " backup.paths else ""
+              then optionalString (backup.paths != null) (concatStringsSep " " backup.paths)
               else "--files-from ${filesFromTmpFile}";
             pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
-              (resticCmd + " forget --prune --cache-dir=%C/restic-backups-${name} " + (concatStringsSep " " backup.pruneOpts))
-              (resticCmd + " check --cache-dir=%C/restic-backups-${name} " + (concatStringsSep " " backup.checkOpts))
+              (resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts))
+              (resticCmd + " check " + (concatStringsSep " " backup.checkOpts))
             ];
             # Helper functions for rclone remotes
             rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1;
@@ -293,6 +316,7 @@ in
           in
           nameValuePair "restic-backups-${name}" ({
             environment = {
+              RESTIC_CACHE_DIR = "%C/restic-backups-${name}";
               RESTIC_PASSWORD_FILE = backup.passwordFile;
               RESTIC_REPOSITORY = backup.repository;
               RESTIC_REPOSITORY_FILE = backup.repositoryFile;
@@ -311,12 +335,13 @@ in
             restartIfChanged = false;
             serviceConfig = {
               Type = "oneshot";
-              ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ])
+              ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup ${concatStringsSep " " (backup.extraBackupArgs ++ excludeFlags)} ${backupPaths}" ])
                 ++ pruneCmd;
               User = backup.user;
               RuntimeDirectory = "restic-backups-${name}";
               CacheDirectory = "restic-backups-${name}";
               CacheDirectoryMode = "0700";
+              PrivateTmp = true;
             } // optionalAttrs (backup.environmentFile != null) {
               EnvironmentFile = backup.environmentFile;
             };
diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix
index a51708170fb..aae77cee07d 100644
--- a/nixos/modules/services/backup/sanoid.nix
+++ b/nixos/modules/services/backup/sanoid.nix
@@ -114,6 +114,8 @@ in
   options.services.sanoid = {
     enable = mkEnableOption (lib.mdDoc "Sanoid ZFS snapshotting service");
 
+    package = lib.mkPackageOptionMD pkgs "sanoid" {};
+
     interval = mkOption {
       type = types.str;
       default = "hourly";
@@ -181,7 +183,7 @@ in
         ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets);
         ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets);
         ExecStart = lib.escapeShellArgs ([
-          "${pkgs.sanoid}/bin/sanoid"
+          "${cfg.package}/bin/sanoid"
           "--cron"
           "--configdir"
           (pkgs.writeTextDir "sanoid.conf" configFile)
diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix
index 6188f109463..0f375455e7e 100644
--- a/nixos/modules/services/backup/syncoid.nix
+++ b/nixos/modules/services/backup/syncoid.nix
@@ -87,6 +87,8 @@ in
   options.services.syncoid = {
     enable = mkEnableOption (lib.mdDoc "Syncoid ZFS synchronization service");
 
+    package = lib.mkPackageOptionMD pkgs "sanoid" {};
+
     interval = mkOption {
       type = types.str;
       default = "hourly";
@@ -331,7 +333,7 @@ in
               ExecStopPost =
                 (map (buildUnallowCommand c.localSourceAllow) (localDatasetName c.source)) ++
                 (map (buildUnallowCommand c.localTargetAllow) (localDatasetName c.target));
-              ExecStart = lib.escapeShellArgs ([ "${pkgs.sanoid}/bin/syncoid" ]
+              ExecStart = lib.escapeShellArgs ([ "${cfg.package}/bin/syncoid" ]
                 ++ optionals c.useCommonArgs cfg.commonArgs
                 ++ optional c.recursive "-r"
                 ++ optionals (c.sshKey != null) [ "--sshkey" c.sshKey ]
diff --git a/nixos/modules/services/backup/zfs-replication.nix b/nixos/modules/services/backup/zfs-replication.nix
index ce914003c62..8e7059e5b59 100644
--- a/nixos/modules/services/backup/zfs-replication.nix
+++ b/nixos/modules/services/backup/zfs-replication.nix
@@ -9,7 +9,7 @@ let
 in {
   options = {
     services.zfs.autoReplication = {
-      enable = mkEnableOption (lib.mdDoc "ZFS snapshot replication.");
+      enable = mkEnableOption (lib.mdDoc "ZFS snapshot replication");
 
       followDelete = mkOption {
         description = lib.mdDoc "Remove remote snapshots that don't have a local correspondent.";
diff --git a/nixos/modules/services/blockchain/ethereum/geth.nix b/nixos/modules/services/blockchain/ethereum/geth.nix
index eca308dc366..d12516ca2f2 100644
--- a/nixos/modules/services/blockchain/ethereum/geth.nix
+++ b/nixos/modules/services/blockchain/ethereum/geth.nix
@@ -196,9 +196,9 @@ in
           --gcmode ${cfg.gcmode} \
           --port ${toString cfg.port} \
           --maxpeers ${toString cfg.maxpeers} \
-          ${if cfg.http.enable then ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}'' else ""} \
+          ${optionalString cfg.http.enable ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}''} \
           ${optionalString (cfg.http.apis != null) ''--http.api ${lib.concatStringsSep "," cfg.http.apis}''} \
-          ${if cfg.websocket.enable then ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}'' else ""} \
+          ${optionalString cfg.websocket.enable ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}''} \
           ${optionalString (cfg.websocket.apis != null) ''--ws.api ${lib.concatStringsSep "," cfg.websocket.apis}''} \
           ${optionalString cfg.metrics.enable ''--metrics --metrics.addr ${cfg.metrics.address} --metrics.port ${toString cfg.metrics.port}''} \
           --authrpc.addr ${cfg.authrpc.address} --authrpc.port ${toString cfg.authrpc.port} --authrpc.vhosts ${lib.concatStringsSep "," cfg.authrpc.vhosts} \
diff --git a/nixos/modules/services/cluster/hadoop/hbase.nix b/nixos/modules/services/cluster/hadoop/hbase.nix
index 97951ebfe33..a39da2a84ec 100644
--- a/nixos/modules/services/cluster/hadoop/hbase.nix
+++ b/nixos/modules/services/cluster/hadoop/hbase.nix
@@ -5,11 +5,95 @@ let
   cfg = config.services.hadoop;
   hadoopConf = "${import ./conf.nix { inherit cfg pkgs lib; }}/";
   mkIfNotNull = x: mkIf (x != null) x;
+  # generic hbase role options
+  hbaseRoleOption = name: extraOpts: {
+    enable = mkEnableOption (mdDoc "HBase ${name}");
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = mdDoc "Open firewall ports for HBase ${name}.";
+    };
+
+    restartIfChanged = mkOption {
+      type = types.bool;
+      default = false;
+      description = mdDoc "Restart ${name} con config change.";
+    };
+
+    extraFlags = mkOption {
+      type = with types; listOf str;
+      default = [];
+      example = literalExpression ''[ "--backup" ]'';
+      description = mdDoc "Extra flags for the ${name} service.";
+    };
+
+    environment = mkOption {
+      type = with types; attrsOf str;
+      default = {};
+      example = literalExpression ''
+        {
+          HBASE_MASTER_OPTS = "-Dcom.sun.management.jmxremote.ssl=true";
+        }
+      '';
+      description = mdDoc "Environment variables passed to ${name}.";
+    };
+  } // extraOpts;
+  # generic hbase role configs
+  hbaseRoleConfig = name: ports: (mkIf cfg.hbase."${name}".enable {
+    services.hadoop.gatewayRole = {
+      enable = true;
+      enableHbaseCli = mkDefault true;
+    };
+
+    systemd.services."hbase-${toLower name}" = {
+      description = "HBase ${name}";
+      wantedBy = [ "multi-user.target" ];
+      path = with cfg; [ hbase.package ] ++ optional
+        (with cfg.hbase.master; enable && initHDFS) package;
+      preStart = mkIf (with cfg.hbase.master; enable && initHDFS)
+        (concatStringsSep "\n" (
+          map (x: "HADOOP_USER_NAME=hdfs hdfs --config /etc/hadoop-conf ${x}")[
+            "dfsadmin -safemode wait"
+            "dfs -mkdir -p ${cfg.hbase.rootdir}"
+            "dfs -chown hbase ${cfg.hbase.rootdir}"
+          ]
+        ));
+
+      inherit (cfg.hbase."${name}") environment;
+      script = concatStringsSep " " (
+        [
+          "hbase --config /etc/hadoop-conf/"
+          "${toLower name} start"
+        ]
+        ++ cfg.hbase."${name}".extraFlags
+        ++ map (x: "--${toLower x} ${toString cfg.hbase.${name}.${x}}")
+          (filter (x: hasAttr x cfg.hbase.${name}) ["port" "infoPort"])
+      );
+
+      serviceConfig = {
+        User = "hbase";
+        SyslogIdentifier = "hbase-${toLower name}";
+        Restart = "always";
+      };
+    };
+
+    services.hadoop.hbaseSiteInternal."hbase.rootdir" = cfg.hbase.rootdir;
+
+    networking = {
+      firewall.allowedTCPPorts = mkIf cfg.hbase."${name}".openFirewall ports;
+      hosts = mkIf (with cfg.hbase.regionServer; enable && overrideHosts) {
+        "127.0.0.2" = mkForce [ ];
+        "::1" = mkForce [ ];
+      };
+    };
+
+  });
 in
 {
   options.services.hadoop = {
 
-    gatewayRole.enableHbaseCli = mkEnableOption (lib.mdDoc "HBase CLI tools");
+    gatewayRole.enableHbaseCli = mkEnableOption (mdDoc "HBase CLI tools");
 
     hbaseSiteDefault = mkOption {
       default = {
@@ -21,7 +105,7 @@ in
         "hbase.cluster.distributed" = "true";
       };
       type = types.attrsOf types.anything;
-      description = lib.mdDoc ''
+      description = mdDoc ''
         Default options for hbase-site.xml
       '';
     };
@@ -29,8 +113,12 @@ in
       default = {};
       type = with types; attrsOf anything;
       example = literalExpression ''
+        {
+          "hbase.hregion.max.filesize" = 20*1024*1024*1024;
+          "hbase.table.normalization.enabled" = "true";
+        }
       '';
-      description = lib.mdDoc ''
+      description = mdDoc ''
         Additional options and overrides for hbase-site.xml
         <https://github.com/apache/hbase/blob/rel/2.4.11/hbase-common/src/main/resources/hbase-default.xml>
       '';
@@ -39,7 +127,7 @@ in
       default = {};
       type = with types; attrsOf anything;
       internal = true;
-      description = lib.mdDoc ''
+      description = mdDoc ''
         Internal option to add configs to hbase-site.xml based on module options
       '';
     };
@@ -50,11 +138,11 @@ in
         type = types.package;
         default = pkgs.hbase;
         defaultText = literalExpression "pkgs.hbase";
-        description = lib.mdDoc "HBase package";
+        description = mdDoc "HBase package";
       };
 
       rootdir = mkOption {
-        description = lib.mdDoc ''
+        description = mdDoc ''
           This option will set "hbase.rootdir" in hbase-site.xml and determine
           the directory shared by region servers and into which HBase persists.
           The URL should be 'fully-qualified' to include the filesystem scheme.
@@ -68,7 +156,7 @@ in
         default = "/hbase";
       };
       zookeeperQuorum = mkOption {
-        description = lib.mdDoc ''
+        description = mdDoc ''
           This option will set "hbase.zookeeper.quorum" in hbase-site.xml.
           Comma separated list of servers in the ZooKeeper ensemble.
         '';
@@ -76,107 +164,36 @@ in
         example = "zk1.internal,zk2.internal,zk3.internal";
         default = null;
       };
-      master = {
-        enable = mkEnableOption (lib.mdDoc "HBase Master");
-        initHDFS = mkEnableOption (lib.mdDoc "initialization of the hbase directory on HDFS");
-
-        openFirewall = mkOption {
-          type = types.bool;
-          default = false;
-          description = lib.mdDoc ''
-            Open firewall ports for HBase master.
-          '';
+    } // (let
+      ports = port: infoPort: {
+        port = mkOption {
+          type = types.int;
+          default = port;
+          description = mdDoc "RPC port";
         };
-      };
-      regionServer = {
-        enable = mkEnableOption (lib.mdDoc "HBase RegionServer");
-
-        overrideHosts = mkOption {
-          type = types.bool;
-          default = true;
-          description = lib.mdDoc ''
-            Remove /etc/hosts entries for "127.0.0.2" and "::1" defined in nixos/modules/config/networking.nix
-            Regionservers must be able to resolve their hostnames to their IP addresses, through PTR records
-            or /etc/hosts entries.
-
-          '';
-        };
-
-        openFirewall = mkOption {
-          type = types.bool;
-          default = false;
-          description = lib.mdDoc ''
-            Open firewall ports for HBase master.
-          '';
+        infoPort = mkOption {
+          type = types.int;
+          default = infoPort;
+          description = mdDoc "web UI port";
         };
       };
-    };
-  };
-
-  config = mkMerge [
-    (mkIf cfg.hbase.master.enable {
-      services.hadoop.gatewayRole = {
-        enable = true;
-        enableHbaseCli = mkDefault true;
-      };
-
-      systemd.services.hbase-master = {
-        description = "HBase master";
-        wantedBy = [ "multi-user.target" ];
-
-        preStart = mkIf cfg.hbase.master.initHDFS ''
-          HADOOP_USER_NAME=hdfs ${cfg.package}/bin/hdfs --config ${hadoopConf} dfsadmin -safemode wait
-          HADOOP_USER_NAME=hdfs ${cfg.package}/bin/hdfs --config ${hadoopConf} dfs -mkdir -p ${cfg.hbase.rootdir}
-          HADOOP_USER_NAME=hdfs ${cfg.package}/bin/hdfs --config ${hadoopConf} dfs -chown hbase ${cfg.hbase.rootdir}
+    in mapAttrs hbaseRoleOption {
+      master.initHDFS = mkEnableOption (mdDoc "initialization of the hbase directory on HDFS");
+      regionServer.overrideHosts = mkOption {
+        type = types.bool;
+        default = true;
+        description = mdDoc ''
+          Remove /etc/hosts entries for "127.0.0.2" and "::1" defined in nixos/modules/config/networking.nix
+          Regionservers must be able to resolve their hostnames to their IP addresses, through PTR records
+          or /etc/hosts entries.
         '';
-
-        serviceConfig = {
-          User = "hbase";
-          SyslogIdentifier = "hbase-master";
-          ExecStart = "${cfg.hbase.package}/bin/hbase --config ${hadoopConf} " +
-                      "master start";
-          Restart = "always";
-        };
-      };
-
-      services.hadoop.hbaseSiteInternal."hbase.rootdir" = cfg.hbase.rootdir;
-
-      networking.firewall.allowedTCPPorts = mkIf cfg.hbase.master.openFirewall [
-        16000 16010
-      ];
-
-    })
-
-    (mkIf cfg.hbase.regionServer.enable {
-      services.hadoop.gatewayRole = {
-        enable = true;
-        enableHbaseCli = mkDefault true;
-      };
-
-      systemd.services.hbase-regionserver = {
-        description = "HBase RegionServer";
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          User = "hbase";
-          SyslogIdentifier = "hbase-regionserver";
-          ExecStart = "${cfg.hbase.package}/bin/hbase --config /etc/hadoop-conf/ " +
-                      "regionserver start";
-          Restart = "always";
-        };
       };
+      thrift = ports 9090 9095;
+      rest = ports 8080 8085;
+    });
+  };
 
-      services.hadoop.hbaseSiteInternal."hbase.rootdir" = cfg.hbase.rootdir;
-
-      networking = {
-        firewall.allowedTCPPorts = mkIf cfg.hbase.regionServer.openFirewall [
-          16020 16030
-        ];
-        hosts = mkIf cfg.hbase.regionServer.overrideHosts {
-          "127.0.0.2" = mkForce [ ];
-          "::1" = mkForce [ ];
-        };
-      };
-    })
+  config = mkMerge ([
 
     (mkIf cfg.gatewayRole.enable {
 
@@ -192,5 +209,10 @@ in
         isSystemUser = true;
       };
     })
-  ];
+  ] ++ (mapAttrsToList hbaseRoleConfig {
+    master = [ 16000 16010 ];
+    regionServer = [ 16020 16030 ];
+    thrift = with cfg.hbase.thrift; [ port infoPort ];
+    rest = with cfg.hbase.rest; [ port infoPort ];
+  }));
 }
diff --git a/nixos/modules/services/cluster/k3s/default.nix b/nixos/modules/services/cluster/k3s/default.nix
index 693f388de14..72b2f992a33 100644
--- a/nixos/modules/services/cluster/k3s/default.nix
+++ b/nixos/modules/services/cluster/k3s/default.nix
@@ -106,6 +106,14 @@ in
       description = lib.mdDoc "Only run the server. This option only makes sense for a server.";
     };
 
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      description = lib.mdDoc ''
+        File path containing environment variables for configuring the k3s service in the format of an EnvironmentFile. See systemd.exec(5).
+      '';
+      default = null;
+    };
+
     configPath = mkOption {
       type = types.nullOr types.path;
       default = null;
@@ -139,8 +147,8 @@ in
 
     systemd.services.k3s = {
       description = "k3s service";
-      after = [ "network.service" "firewall.service" ];
-      wants = [ "network.service" "firewall.service" ];
+      after = [ "firewall.service" "network-online.target" ];
+      wants = [ "firewall.service" "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       path = optional config.boot.zfs.enabled config.boot.zfs.package;
       serviceConfig = {
@@ -154,6 +162,7 @@ in
         LimitNPROC = "infinity";
         LimitCORE = "infinity";
         TasksMax = "infinity";
+        EnvironmentFile = cfg.environmentFile;
         ExecStart = concatStringsSep " \\\n " (
           [
             "${cfg.package}/bin/k3s ${cfg.role}"
diff --git a/nixos/modules/services/cluster/kubernetes/addon-manager.nix b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
index 7aa2a8323b1..dc851688fbe 100644
--- a/nixos/modules/services/cluster/kubernetes/addon-manager.nix
+++ b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
@@ -62,7 +62,7 @@ in
       '';
     };
 
-    enable = mkEnableOption (lib.mdDoc "Kubernetes addon manager.");
+    enable = mkEnableOption (lib.mdDoc "Kubernetes addon manager");
   };
 
   ###### implementation
diff --git a/nixos/modules/services/cluster/kubernetes/addons/dns.nix b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
index 3d41b5f0085..1c00329e6cc 100644
--- a/nixos/modules/services/cluster/kubernetes/addons/dns.nix
+++ b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
@@ -3,7 +3,7 @@
 with lib;
 
 let
-  version = "1.7.1";
+  version = "1.10.1";
   cfg = config.services.kubernetes.addons.dns;
   ports = {
     dns = 10053;
@@ -59,9 +59,9 @@ in {
       type = types.attrs;
       default = {
         imageName = "coredns/coredns";
-        imageDigest = "sha256:4a6e0769130686518325b21b0c1d0688b54e7c79244d48e1b15634e98e40c6ef";
+        imageDigest = "sha256:a0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e";
         finalImageTag = version;
-        sha256 = "02r440xcdsgi137k5lmmvp0z5w5fmk8g9mysq5pnysq1wl8sj6mw";
+        sha256 = "0wg696920smmal7552a2zdhfncndn5kfammfa8bk8l7dz9bhk0y1";
       };
     };
 
@@ -136,6 +136,11 @@ in {
             resources = [ "nodes" ];
             verbs = [ "get" ];
           }
+          {
+            apiGroups = [ "discovery.k8s.io" ];
+            resources = [ "endpointslices" ];
+            verbs = [ "list" "watch" ];
+          }
         ];
       };
 
diff --git a/nixos/modules/services/cluster/kubernetes/flannel.nix b/nixos/modules/services/cluster/kubernetes/flannel.nix
index 53003287fc9..11c5adc6a88 100644
--- a/nixos/modules/services/cluster/kubernetes/flannel.nix
+++ b/nixos/modules/services/cluster/kubernetes/flannel.nix
@@ -53,7 +53,7 @@ in
       };
     };
 
-    # give flannel som kubernetes rbac permissions if applicable
+    # give flannel some kubernetes rbac permissions if applicable
     services.kubernetes.addonManager.bootstrapAddons = mkIf ((storageBackend == "kubernetes") && (elem "RBAC" top.apiserver.authorizationMode)) {
 
       flannel-cr = {
diff --git a/nixos/modules/services/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix
index 3ede1cb80e8..fd2dce7ee6a 100644
--- a/nixos/modules/services/cluster/kubernetes/kubelet.nix
+++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix
@@ -63,6 +63,7 @@ in
     (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "cadvisorPort" ] "")
     (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "allowPrivileged" ] "")
     (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "networkPlugin" ] "")
+    (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "containerRuntime" ] "")
   ];
 
   ###### interface
@@ -134,19 +135,13 @@ in
       };
     };
 
-    containerRuntime = mkOption {
-      description = lib.mdDoc "Which container runtime type to use";
-      type = enum ["docker" "remote"];
-      default = "remote";
-    };
-
     containerRuntimeEndpoint = mkOption {
       description = lib.mdDoc "Endpoint at which to find the container runtime api interface/socket";
       type = str;
       default = "unix:///run/containerd/containerd.sock";
     };
 
-    enable = mkEnableOption (lib.mdDoc "Kubernetes kubelet.");
+    enable = mkEnableOption (lib.mdDoc "Kubernetes kubelet");
 
     extraOpts = mkOption {
       description = lib.mdDoc "Kubernetes kubelet extra command line options.";
@@ -331,7 +326,6 @@ in
             ${optionalString (cfg.tlsKeyFile != null)
               "--tls-private-key-file=${cfg.tlsKeyFile}"} \
             ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
-            --container-runtime=${cfg.containerRuntime} \
             --container-runtime-endpoint=${cfg.containerRuntimeEndpoint} \
             --cgroup-driver=systemd \
             ${cfg.extraOpts}
@@ -343,7 +337,7 @@ in
         };
       };
 
-      # Allways include cni plugins
+      # Always include cni plugins
       services.kubernetes.kubelet.cni.packages = [pkgs.cni-plugins pkgs.cni-plugin-flannel];
 
       boot.kernelModules = ["br_netfilter" "overlay"];
diff --git a/nixos/modules/services/cluster/kubernetes/pki.nix b/nixos/modules/services/cluster/kubernetes/pki.nix
index 26fe0f5e9e0..38682701ea1 100644
--- a/nixos/modules/services/cluster/kubernetes/pki.nix
+++ b/nixos/modules/services/cluster/kubernetes/pki.nix
@@ -270,7 +270,7 @@ in
           '';
         })]);
 
-      environment.etc.${cfg.etcClusterAdminKubeconfig}.source = mkIf (!isNull cfg.etcClusterAdminKubeconfig)
+      environment.etc.${cfg.etcClusterAdminKubeconfig}.source = mkIf (cfg.etcClusterAdminKubeconfig != null)
         clusterAdminKubeconfig;
 
       environment.systemPackages = mkIf (top.kubelet.enable || top.proxy.enable) [
diff --git a/nixos/modules/services/computing/boinc/client.nix b/nixos/modules/services/computing/boinc/client.nix
index 5fb715f4d77..1879fef9666 100644
--- a/nixos/modules/services/computing/boinc/client.nix
+++ b/nixos/modules/services/computing/boinc/client.nix
@@ -6,7 +6,7 @@ let
   cfg = config.services.boinc;
   allowRemoteGuiRpcFlag = optionalString cfg.allowRemoteGuiRpc "--allow_remote_gui_rpc";
 
-  fhsEnv = pkgs.buildFHSUserEnv {
+  fhsEnv = pkgs.buildFHSEnv {
     name = "boinc-fhs-env";
     targetPkgs = pkgs': [ cfg.package ] ++ cfg.extraEnvPackages;
     runScript = "/bin/boinc_client";
diff --git a/nixos/modules/services/computing/slurm/slurm.nix b/nixos/modules/services/computing/slurm/slurm.nix
index 0c80e79d4b7..344c43a429b 100644
--- a/nixos/modules/services/computing/slurm/slurm.nix
+++ b/nixos/modules/services/computing/slurm/slurm.nix
@@ -383,7 +383,7 @@ in
       "d /var/spool/slurmd 755 root root -"
     ];
 
-    services.openssh.forwardX11 = mkIf cfg.client.enable (mkDefault true);
+    services.openssh.settings.X11Forwarding = mkIf cfg.client.enable (mkDefault true);
 
     systemd.services.slurmctld = mkIf (cfg.server.enable) {
       path = with pkgs; [ wrappedSlurm munge coreutils ]
diff --git a/nixos/modules/services/continuous-integration/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix
index 5666199c484..595374ea1e5 100644
--- a/nixos/modules/services/continuous-integration/buildbot/master.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/master.nix
@@ -8,7 +8,8 @@ let
   cfg = config.services.buildbot-master;
   opt = options.services.buildbot-master;
 
-  python = cfg.package.pythonModule;
+  package = pkgs.python3.pkgs.toPythonModule cfg.package;
+  python = package.pythonModule;
 
   escapeStr = escape [ "'" ];
 
@@ -212,10 +213,10 @@ in {
 
       package = mkOption {
         type = types.package;
-        default = pkgs.python3Packages.buildbot-full;
-        defaultText = literalExpression "pkgs.python3Packages.buildbot-full";
+        default = pkgs.buildbot-full;
+        defaultText = literalExpression "pkgs.buildbot-full";
         description = lib.mdDoc "Package to use for buildbot.";
-        example = literalExpression "pkgs.python3Packages.buildbot";
+        example = literalExpression "pkgs.buildbot";
       };
 
       packages = mkOption {
@@ -255,7 +256,7 @@ in {
       after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       path = cfg.packages ++ cfg.pythonPackages python.pkgs;
-      environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ cfg.package ])}/${python.sitePackages}";
+      environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ package ])}/${python.sitePackages}";
 
       preStart = ''
         mkdir -vp "${cfg.buildbotDir}"
diff --git a/nixos/modules/services/continuous-integration/buildbot/worker.nix b/nixos/modules/services/continuous-integration/buildbot/worker.nix
index 52c41c4a758..7e78b8935f8 100644
--- a/nixos/modules/services/continuous-integration/buildbot/worker.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/worker.nix
@@ -8,7 +8,8 @@ let
   cfg = config.services.buildbot-worker;
   opt = options.services.buildbot-worker;
 
-  python = cfg.package.pythonModule;
+  package = pkgs.python3.pkgs.toPythonModule cfg.package;
+  python = package.pythonModule;
 
   tacFile = pkgs.writeText "aur-buildbot-worker.tac" ''
     import os
@@ -129,7 +130,7 @@ in {
 
       package = mkOption {
         type = types.package;
-        default = pkgs.python3Packages.buildbot-worker;
+        default = pkgs.buildbot-worker;
         defaultText = literalExpression "pkgs.python3Packages.buildbot-worker";
         description = lib.mdDoc "Package to use for buildbot worker.";
         example = literalExpression "pkgs.python2Packages.buildbot-worker";
@@ -168,7 +169,7 @@ in {
       after = [ "network.target" "buildbot-master.service" ];
       wantedBy = [ "multi-user.target" ];
       path = cfg.packages;
-      environment.PYTHONPATH = "${python.withPackages (p: [ cfg.package ])}/${python.sitePackages}";
+      environment.PYTHONPATH = "${python.withPackages (p: [ package ])}/${python.sitePackages}";
 
       preStart = ''
         mkdir -vp "${cfg.buildbotDir}/info"
diff --git a/nixos/modules/services/continuous-integration/buildkite-agents.nix b/nixos/modules/services/continuous-integration/buildkite-agents.nix
index 7c8f77580ff..a40b939a16c 100644
--- a/nixos/modules/services/continuous-integration/buildkite-agents.nix
+++ b/nixos/modules/services/continuous-integration/buildkite-agents.nix
@@ -11,7 +11,7 @@ let
       default = null;
       description = lib.mdDoc description;
       type = types.nullOr types.lines;
-    } // (if example == null then {} else { inherit example; });
+    } // (lib.optionalAttrs (example != null) { inherit example; });
   };
   mkHookOptions = hooks: listToAttrs (map mkHookOption hooks);
 
diff --git a/nixos/modules/services/continuous-integration/gitea-actions-runner.nix b/nixos/modules/services/continuous-integration/gitea-actions-runner.nix
new file mode 100644
index 00000000000..fb70c489912
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/gitea-actions-runner.nix
@@ -0,0 +1,237 @@
+{ config
+, lib
+, pkgs
+, utils
+, ...
+}:
+
+let
+  inherit (lib)
+    any
+    attrValues
+    concatStringsSep
+    escapeShellArg
+    hasInfix
+    hasSuffix
+    optionalAttrs
+    optionals
+    literalExpression
+    mapAttrs'
+    mkEnableOption
+    mkOption
+    mkPackageOptionMD
+    mkIf
+    nameValuePair
+    types
+  ;
+
+  inherit (utils)
+    escapeSystemdPath
+  ;
+
+  cfg = config.services.gitea-actions-runner;
+
+  # Check whether any runner instance label requires a container runtime
+  # Empty label strings result in the upstream defined defaultLabels, which require docker
+  # https://gitea.com/gitea/act_runner/src/tag/v0.1.5/internal/app/cmd/register.go#L93-L98
+  hasDockerScheme = instance:
+    instance.labels == [] || any (label: hasInfix ":docker:" label) instance.labels;
+  wantsContainerRuntime = any hasDockerScheme (attrValues cfg.instances);
+
+  hasHostScheme = instance: any (label: hasSuffix ":host" label) instance.labels;
+
+  # provide shorthands for whether container runtimes are enabled
+  hasDocker = config.virtualisation.docker.enable;
+  hasPodman = config.virtualisation.podman.enable;
+
+  tokenXorTokenFile = instance:
+    (instance.token == null && instance.tokenFile != null) ||
+    (instance.token != null && instance.tokenFile == null);
+in
+{
+  meta.maintainers = with lib.maintainers; [
+    hexa
+  ];
+
+  options.services.gitea-actions-runner = with types; {
+    package = mkPackageOptionMD pkgs "gitea-actions-runner" { };
+
+    instances = mkOption {
+      default = {};
+      description = lib.mdDoc ''
+        Gitea Actions Runner instances.
+      '';
+      type = attrsOf (submodule {
+        options = {
+          enable = mkEnableOption (lib.mdDoc "Gitea Actions Runner instance");
+
+          name = mkOption {
+            type = str;
+            example = literalExpression "config.networking.hostName";
+            description = lib.mdDoc ''
+              The name identifying the runner instance towards the Gitea/Forgejo instance.
+            '';
+          };
+
+          url = mkOption {
+            type = str;
+            example = "https://forge.example.com";
+            description = lib.mdDoc ''
+              Base URL of your Gitea/Forgejo instance.
+            '';
+          };
+
+          token = mkOption {
+            type = nullOr str;
+            default = null;
+            description = lib.mdDoc ''
+              Plain token to register at the configured Gitea/Forgejo instance.
+            '';
+          };
+
+          tokenFile = mkOption {
+            type = nullOr (either str path);
+            default = null;
+            description = lib.mdDoc ''
+              Path to an environment file, containing the `TOKEN` environment
+              variable, that holds a token to register at the configured
+              Gitea/Forgejo instance.
+            '';
+          };
+
+          labels = mkOption {
+            type = listOf str;
+            example = literalExpression ''
+              [
+                # provide a debian base with nodejs for actions
+                "debian-latest:docker://node:18-bullseye"
+                # fake the ubuntu name, because node provides no ubuntu builds
+                "ubuntu-latest:docker://node:18-bullseye"
+                # provide native execution on the host
+                #"native:host"
+              ]
+            '';
+            description = lib.mdDoc ''
+              Labels used to map jobs to their runtime environment. Changing these
+              labels currently requires a new registration token.
+
+              Many common actions require bash, git and nodejs, as well as a filesystem
+              that follows the filesystem hierarchy standard.
+            '';
+          };
+
+          hostPackages = mkOption {
+            type = listOf package;
+            default = with pkgs; [
+              bash
+              coreutils
+              curl
+              gawk
+              gitMinimal
+              gnused
+              nodejs
+              wget
+            ];
+            defaultText = literalExpression ''
+              with pkgs; [
+                bash
+                coreutils
+                curl
+                gawk
+                gitMinimal
+                gnused
+                nodejs
+                wget
+              ]
+            '';
+            description = lib.mdDoc ''
+              List of packages, that are available to actions, when the runner is configured
+              with a host execution label.
+            '';
+          };
+        };
+      });
+    };
+  };
+
+  config = mkIf (cfg.instances != {}) {
+    assertions = [ {
+      assertion = any tokenXorTokenFile (attrValues cfg.instances);
+      message = "Instances of gitea-actions-runner can have `token` or `tokenFile`, not both.";
+    } {
+      assertion = wantsContainerRuntime -> hasDocker || hasPodman;
+      message = "Label configuration on gitea-actions-runner instance requires either docker or podman.";
+    } ];
+
+    systemd.services = let
+      mkRunnerService = name: instance: let
+        wantsContainerRuntime = hasDockerScheme instance;
+        wantsHost = hasHostScheme instance;
+        wantsDocker = wantsContainerRuntime && config.virtualisation.docker.enable;
+        wantsPodman = wantsContainerRuntime && config.virtualisation.podman.enable;
+      in
+        nameValuePair "gitea-runner-${escapeSystemdPath name}" {
+          inherit (instance) enable;
+          description = "Gitea Actions Runner";
+          after = [
+            "network-online.target"
+          ] ++ optionals (wantsDocker) [
+            "docker.service"
+          ] ++ optionals (wantsPodman) [
+            "podman.service"
+          ];
+          wantedBy = [
+            "multi-user.target"
+          ];
+          environment = optionalAttrs (instance.token != null) {
+            TOKEN = "${instance.token}";
+          } // optionalAttrs (wantsPodman) {
+            DOCKER_HOST = "unix:///run/podman/podman.sock";
+          };
+          path = with pkgs; [
+            coreutils
+          ] ++ lib.optionals wantsHost instance.hostPackages;
+          serviceConfig = {
+            DynamicUser = true;
+            User = "gitea-runner";
+            StateDirectory = "gitea-runner";
+            WorkingDirectory = "-/var/lib/gitea-runner/${name}";
+            ExecStartPre = pkgs.writeShellScript "gitea-register-runner-${name}" ''
+              export INSTANCE_DIR="$STATE_DIRECTORY/${name}"
+              mkdir -vp "$INSTANCE_DIR"
+              cd "$INSTANCE_DIR"
+
+              # force reregistration on changed labels
+              export LABELS_FILE="$INSTANCE_DIR/.labels"
+              export LABELS_WANTED="$(echo ${escapeShellArg (concatStringsSep "\n" instance.labels)} | sort)"
+              export LABELS_CURRENT="$(cat $LABELS_FILE 2>/dev/null || echo 0)"
+
+              if [ ! -e "$INSTANCE_DIR/.runner" ] || [ "$LABELS_WANTED" != "$LABELS_CURRENT" ]; then
+                # remove existing registration file, so that changing the labels forces a re-registration
+                rm -v "$INSTANCE_DIR/.runner" || true
+
+                # perform the registration
+                ${cfg.package}/bin/act_runner register --no-interactive \
+                  --instance ${escapeShellArg instance.url} \
+                  --token "$TOKEN" \
+                  --name ${escapeShellArg instance.name} \
+                  --labels ${escapeShellArg (concatStringsSep "," instance.labels)}
+
+                # and write back the configured labels
+                echo "$LABELS_WANTED" > "$LABELS_FILE"
+              fi
+
+            '';
+            ExecStart = "${cfg.package}/bin/act_runner daemon";
+            SupplementaryGroups = optionals (wantsDocker) [
+              "docker"
+            ] ++ optionals (wantsPodman) [
+              "podman"
+            ];
+          } // optionalAttrs (instance.tokenFile != null) {
+            EnvironmentFile = instance.tokenFile;
+          };
+        };
+    in mapAttrs' mkRunnerService cfg.instances;
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/github-runner.nix b/nixos/modules/services/continuous-integration/github-runner.nix
index 24d02c931a4..27cfee92c75 100644
--- a/nixos/modules/services/continuous-integration/github-runner.nix
+++ b/nixos/modules/services/continuous-integration/github-runner.nix
@@ -20,4 +20,6 @@ in
   config = mkIf cfg.enable {
     services.github-runners.${cfg.name} = cfg;
   };
+
+  meta.maintainers = with maintainers; [ veehaitch newam thomasjm ];
 }
diff --git a/nixos/modules/services/continuous-integration/github-runner/options.nix b/nixos/modules/services/continuous-integration/github-runner/options.nix
index 72ac0c12990..ce880921372 100644
--- a/nixos/modules/services/continuous-integration/github-runner/options.nix
+++ b/nixos/modules/services/continuous-integration/github-runner/options.nix
@@ -41,17 +41,42 @@ with lib;
   tokenFile = mkOption {
     type = types.path;
     description = lib.mdDoc ''
-      The full path to a file which contains either a runner registration token or a
-      (fine-grained) personal access token (PAT).
+      The full path to a file which contains either
+
+      * a fine-grained personal access token (PAT),
+      * a classic PAT
+      * or a runner registration token
+
+      Changing this option or the `tokenFile`’s content triggers a new runner registration.
+
+      We suggest using the fine-grained PATs. A runner registration token is valid
+      only for 1 hour after creation, so the next time the runner configuration changes
+      this will give you hard-to-debug HTTP 404 errors in the configure step.
+
       The file should contain exactly one line with the token without any newline.
+      (Use `echo -n '…token…' > …token file…` to make sure no newlines sneak in.)
+
+      If the file contains a PAT, the service creates a new registration token
+      on startup as needed.
       If a registration token is given, it can be used to re-register a runner of the same
-      name but is time-limited. If the file contains a PAT, the service creates a new
-      registration token on startup as needed. Make sure the PAT has a scope of
-      `admin:org` for organization-wide registrations or a scope of
-      `repo` for a single repository. Fine-grained PATs need read and write permission
-      to the "Administration" resources.
+      name but is time-limited as noted above.
+
+      For fine-grained PATs:
+
+      Give it "Read and Write access to organization/repository self hosted runners",
+      depending on whether it is organization wide or per-repository. You might have to
+      experiment a little, fine-grained PATs are a `beta` Github feature and still subject
+      to change; nonetheless they are the best option at the moment.
+
+      For classic PATs:
 
-      Changing this option or the file's content triggers a new runner registration.
+      Make sure the PAT has a scope of `admin:org` for organization-wide registrations
+      or a scope of `repo` for a single repository.
+
+      For runner registration tokens:
+
+      Nothing special needs to be done, but updating will break after one hour,
+      so these are not recommended.
     '';
     example = "/run/secrets/github-runner/nixos.token";
   };
@@ -127,10 +152,11 @@ with lib;
   serviceOverrides = mkOption {
     type = types.attrs;
     description = lib.mdDoc ''
-      Overrides for the systemd service. Can be used to adjust the sandboxing options.
+      Modify the systemd service. Can be used to, e.g., adjust the sandboxing options.
     '';
     example = {
       ProtectHome = false;
+      RestrictAddressFamilies = [ "AF_PACKET" ];
     };
     default = {};
   };
@@ -170,4 +196,16 @@ with lib;
     default = null;
     defaultText = literalExpression "username";
   };
+
+  workDir = mkOption {
+    type = with types; nullOr str;
+    description = lib.mdDoc ''
+      Working directory, available as `$GITHUB_WORKSPACE` during workflow runs
+      and used as a default for [repository checkouts](https://github.com/actions/checkout).
+      The service cleans this directory on every service start.
+
+      A value of `null` will default to the systemd `RuntimeDirectory`.
+    '';
+    default = null;
+  };
 }
diff --git a/nixos/modules/services/continuous-integration/github-runner/service.nix b/nixos/modules/services/continuous-integration/github-runner/service.nix
index cd81631582f..55df83362cb 100644
--- a/nixos/modules/services/continuous-integration/github-runner/service.nix
+++ b/nixos/modules/services/continuous-integration/github-runner/service.nix
@@ -20,6 +20,9 @@
 
 with lib;
 
+let
+  workDir = if cfg.workDir == null then runtimeDir else cfg.workDir;
+in
 {
   description = "GitHub Actions runner";
 
@@ -28,7 +31,7 @@ with lib;
   after = [ "network.target" "network-online.target" ];
 
   environment = {
-    HOME = runtimeDir;
+    HOME = workDir;
     RUNNER_ROOT = stateDir;
   } // cfg.extraEnvironment;
 
@@ -42,216 +45,223 @@ with lib;
     config.nix.package
   ] ++ cfg.extraPackages;
 
-  serviceConfig = rec {
-    ExecStart = "${cfg.package}/bin/Runner.Listener run --startuptype service";
+  serviceConfig = mkMerge [
+    {
+      ExecStart = "${cfg.package}/bin/Runner.Listener run --startuptype service";
 
-    # Does the following, sequentially:
-    # - If the module configuration or the token has changed, purge the state directory,
-    #   and create the current and the new token file with the contents of the configured
-    #   token. While both files have the same content, only the later is accessible by
-    #   the service user.
-    # - Configure the runner using the new token file. When finished, delete it.
-    # - Set up the directory structure by creating the necessary symlinks.
-    ExecStartPre =
-      let
-        # Wrapper script which expects the full path of the state, runtime and logs
-        # directory as arguments. Overrides the respective systemd variables to provide
-        # unambiguous directory names. This becomes relevant, for example, if the
-        # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory=
-        # to contain more than one directory. This causes systemd to set the respective
-        # environment variables with the path of all of the given directories, separated
-        # by a colon.
-        writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" ''
-          set -euo pipefail
+      # Does the following, sequentially:
+      # - If the module configuration or the token has changed, purge the state directory,
+      #   and create the current and the new token file with the contents of the configured
+      #   token. While both files have the same content, only the later is accessible by
+      #   the service user.
+      # - Configure the runner using the new token file. When finished, delete it.
+      # - Set up the directory structure by creating the necessary symlinks.
+      ExecStartPre =
+        let
+          # Wrapper script which expects the full path of the state, working and logs
+          # directory as arguments. Overrides the respective systemd variables to provide
+          # unambiguous directory names. This becomes relevant, for example, if the
+          # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory=
+          # to contain more than one directory. This causes systemd to set the respective
+          # environment variables with the path of all of the given directories, separated
+          # by a colon.
+          writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" ''
+            set -euo pipefail
 
-          STATE_DIRECTORY="$1"
-          RUNTIME_DIRECTORY="$2"
-          LOGS_DIRECTORY="$3"
+            STATE_DIRECTORY="$1"
+            WORK_DIRECTORY="$2"
+            LOGS_DIRECTORY="$3"
 
-          ${lines}
-        '';
-        runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" "ephemeral" ] cfg;
-        newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
-        currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
-        newConfigTokenPath= "$STATE_DIRECTORY/.new-token";
-        currentConfigTokenPath = "$STATE_DIRECTORY/${currentConfigTokenFilename}";
+            ${lines}
+          '';
+          runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" "ephemeral" "workDir" ] cfg;
+          newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
+          currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
+          newConfigTokenPath = "$STATE_DIRECTORY/.new-token";
+          currentConfigTokenPath = "$STATE_DIRECTORY/${currentConfigTokenFilename}";
 
-        runnerCredFiles = [
-          ".credentials"
-          ".credentials_rsaparams"
-          ".runner"
-        ];
-        unconfigureRunner = writeScript "unconfigure" ''
-          copy_tokens() {
-            # Copy the configured token file to the state dir and allow the service user to read the file
-            install --mode=666 ${escapeShellArg cfg.tokenFile} "${newConfigTokenPath}"
-            # Also copy current file to allow for a diff on the next start
-            install --mode=600 ${escapeShellArg cfg.tokenFile} "${currentConfigTokenPath}"
-          }
-          clean_state() {
-            find "$STATE_DIRECTORY/" -mindepth 1 -delete
-            copy_tokens
-          }
-          diff_config() {
-            changed=0
-            # Check for module config changes
-            [[ -f "${currentConfigPath}" ]] \
-              && ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 \
-              || changed=1
-            # Also check the content of the token file
-            [[ -f "${currentConfigTokenPath}" ]] \
-              && ${pkgs.diffutils}/bin/diff -q "${currentConfigTokenPath}" ${escapeShellArg cfg.tokenFile} >/dev/null 2>&1 \
-              || changed=1
-            # If the config has changed, remove old state and copy tokens
-            if [[ "$changed" -eq 1 ]]; then
-              echo "Config has changed, removing old runner state."
-              echo "The old runner will still appear in the GitHub Actions UI." \
-                   "You have to remove it manually."
+          runnerCredFiles = [
+            ".credentials"
+            ".credentials_rsaparams"
+            ".runner"
+          ];
+          unconfigureRunner = writeScript "unconfigure" ''
+            copy_tokens() {
+              # Copy the configured token file to the state dir and allow the service user to read the file
+              install --mode=666 ${escapeShellArg cfg.tokenFile} "${newConfigTokenPath}"
+              # Also copy current file to allow for a diff on the next start
+              install --mode=600 ${escapeShellArg cfg.tokenFile} "${currentConfigTokenPath}"
+            }
+            clean_state() {
+              find "$STATE_DIRECTORY/" -mindepth 1 -delete
+              copy_tokens
+            }
+            diff_config() {
+              changed=0
+              # Check for module config changes
+              [[ -f "${currentConfigPath}" ]] \
+                && ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 \
+                || changed=1
+              # Also check the content of the token file
+              [[ -f "${currentConfigTokenPath}" ]] \
+                && ${pkgs.diffutils}/bin/diff -q "${currentConfigTokenPath}" ${escapeShellArg cfg.tokenFile} >/dev/null 2>&1 \
+                || changed=1
+              # If the config has changed, remove old state and copy tokens
+              if [[ "$changed" -eq 1 ]]; then
+                echo "Config has changed, removing old runner state."
+                echo "The old runner will still appear in the GitHub Actions UI." \
+                     "You have to remove it manually."
+                clean_state
+              fi
+            }
+            if [[ "${optionalString cfg.ephemeral "1"}" ]]; then
+              # In ephemeral mode, we always want to start with a clean state
               clean_state
-            fi
-          }
-          if [[ "${optionalString cfg.ephemeral "1"}" ]]; then
-            # In ephemeral mode, we always want to start with a clean state
-            clean_state
-          elif [[ "$(ls -A "$STATE_DIRECTORY")" ]]; then
-            # There are state files from a previous run; diff them to decide if we need a new registration
-            diff_config
-          else
-            # The state directory is entirely empty which indicates a first start
-            copy_tokens
-          fi        '';
-        configureRunner = writeScript "configure" ''
-          if [[ -e "${newConfigTokenPath}" ]]; then
-            echo "Configuring GitHub Actions Runner"
-            args=(
-              --unattended
-              --disableupdate
-              --work "$RUNTIME_DIRECTORY"
-              --url ${escapeShellArg cfg.url}
-              --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)}
-              --name ${escapeShellArg cfg.name}
-              ${optionalString cfg.replace "--replace"}
-              ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
-              ${optionalString cfg.ephemeral "--ephemeral"}
-            )
-            # If the token file contains a PAT (i.e., it starts with "ghp_" or "github_pat_"), we have to use the --pat option,
-            # if it is not a PAT, we assume it contains a registration token and use the --token option
-            token=$(<"${newConfigTokenPath}")
-            if [[ "$token" =~ ^ghp_* ]] || [[ "$token" =~ ^github_pat_* ]]; then
-              args+=(--pat "$token")
+            elif [[ "$(ls -A "$STATE_DIRECTORY")" ]]; then
+              # There are state files from a previous run; diff them to decide if we need a new registration
+              diff_config
             else
-              args+=(--token "$token")
+              # The state directory is entirely empty which indicates a first start
+              copy_tokens
             fi
-            ${cfg.package}/bin/config.sh "''${args[@]}"
-            # Move the automatically created _diag dir to the logs dir
-            mkdir -p  "$STATE_DIRECTORY/_diag"
-            cp    -r  "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/"
-            rm    -rf "$STATE_DIRECTORY/_diag/"
-            # Cleanup token from config
-            rm "${newConfigTokenPath}"
-            # Symlink to new config
-            ln -s '${newConfigPath}' "${currentConfigPath}"
-          fi
-        '';
-        setupRuntimeDir = writeScript "setup-runtime-dirs" ''
-          # Link _diag dir
-          ln -s "$LOGS_DIRECTORY" "$RUNTIME_DIRECTORY/_diag"
+            # Always clean workDir
+            find -H "$WORK_DIRECTORY" -mindepth 1 -delete
+          '';
+          configureRunner = writeScript "configure" ''
+            if [[ -e "${newConfigTokenPath}" ]]; then
+              echo "Configuring GitHub Actions Runner"
+              args=(
+                --unattended
+                --disableupdate
+                --work "$WORK_DIRECTORY"
+                --url ${escapeShellArg cfg.url}
+                --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)}
+                --name ${escapeShellArg cfg.name}
+                ${optionalString cfg.replace "--replace"}
+                ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
+                ${optionalString cfg.ephemeral "--ephemeral"}
+              )
+              # If the token file contains a PAT (i.e., it starts with "ghp_" or "github_pat_"), we have to use the --pat option,
+              # if it is not a PAT, we assume it contains a registration token and use the --token option
+              token=$(<"${newConfigTokenPath}")
+              if [[ "$token" =~ ^ghp_* ]] || [[ "$token" =~ ^github_pat_* ]]; then
+                args+=(--pat "$token")
+              else
+                args+=(--token "$token")
+              fi
+              ${cfg.package}/bin/Runner.Listener configure "''${args[@]}"
+              # Move the automatically created _diag dir to the logs dir
+              mkdir -p  "$STATE_DIRECTORY/_diag"
+              cp    -r  "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/"
+              rm    -rf "$STATE_DIRECTORY/_diag/"
+              # Cleanup token from config
+              rm "${newConfigTokenPath}"
+              # Symlink to new config
+              ln -s '${newConfigPath}' "${currentConfigPath}"
+            fi
+          '';
+          setupWorkDir = writeScript "setup-work-dirs" ''
+            # Link _diag dir
+            ln -s "$LOGS_DIRECTORY" "$WORK_DIRECTORY/_diag"
 
-          # Link the runner credentials to the runtime dir
-          ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$RUNTIME_DIRECTORY/"
-        '';
-      in
-        map (x: "${x} ${escapeShellArgs [ stateDir runtimeDir logsDir ]}") [
+            # Link the runner credentials to the work dir
+            ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$WORK_DIRECTORY/"
+          '';
+        in
+        map (x: "${x} ${escapeShellArgs [ stateDir workDir logsDir ]}") [
           "+${unconfigureRunner}" # runs as root
           configureRunner
-          setupRuntimeDir
+          setupWorkDir
         ];
 
-    # If running in ephemeral mode, restart the service on-exit (i.e., successful de-registration of the runner)
-    # to trigger a fresh registration.
-    Restart = if cfg.ephemeral then "on-success" else "no";
-    # If the runner exits with `ReturnCode.RetryableError = 2`, always restart the service:
-    # https://github.com/actions/runner/blob/40ed7f8/src/Runner.Common/Constants.cs#L146
-    RestartForceExitStatus = [ 2 ];
+      # If running in ephemeral mode, restart the service on-exit (i.e., successful de-registration of the runner)
+      # to trigger a fresh registration.
+      Restart = if cfg.ephemeral then "on-success" else "no";
+      # If the runner exits with `ReturnCode.RetryableError = 2`, always restart the service:
+      # https://github.com/actions/runner/blob/40ed7f8/src/Runner.Common/Constants.cs#L146
+      RestartForceExitStatus = [ 2 ];
+
+      # Contains _diag
+      LogsDirectory = [ systemdDir ];
+      # Default RUNNER_ROOT which contains ephemeral Runner data
+      RuntimeDirectory = [ systemdDir ];
+      # Home of persistent runner data, e.g., credentials
+      StateDirectory = [ systemdDir ];
+      StateDirectoryMode = "0700";
+      WorkingDirectory = workDir;
 
-    # Contains _diag
-    LogsDirectory = [ systemdDir ];
-    # Default RUNNER_ROOT which contains ephemeral Runner data
-    RuntimeDirectory = [ systemdDir ];
-    # Home of persistent runner data, e.g., credentials
-    StateDirectory = [ systemdDir ];
-    StateDirectoryMode = "0700";
-    WorkingDirectory = runtimeDir;
+      InaccessiblePaths = [
+        # Token file path given in the configuration, if visible to the service
+        "-${cfg.tokenFile}"
+        # Token file in the state directory
+        "${stateDir}/${currentConfigTokenFilename}"
+      ];
 
-    InaccessiblePaths = [
-      # Token file path given in the configuration, if visible to the service
-      "-${cfg.tokenFile}"
-      # Token file in the state directory
-      "${stateDir}/${currentConfigTokenFilename}"
-    ];
+      KillSignal = "SIGINT";
 
-    KillSignal = "SIGINT";
+      # Hardening (may overlap with DynamicUser=)
+      # The following options are only for optimizing:
+      # systemd-analyze security github-runner
+      AmbientCapabilities = mkBefore [ "" ];
+      CapabilityBoundingSet = mkBefore [ "" ];
+      # ProtectClock= adds DeviceAllow=char-rtc r
+      DeviceAllow = mkBefore [ "" ];
+      NoNewPrivileges = mkDefault true;
+      PrivateDevices = mkDefault true;
+      PrivateMounts = mkDefault true;
+      PrivateTmp = mkDefault true;
+      PrivateUsers = mkDefault true;
+      ProtectClock = mkDefault true;
+      ProtectControlGroups = mkDefault true;
+      ProtectHome = mkDefault true;
+      ProtectHostname = mkDefault true;
+      ProtectKernelLogs = mkDefault true;
+      ProtectKernelModules = mkDefault true;
+      ProtectKernelTunables = mkDefault true;
+      ProtectSystem = mkDefault "strict";
+      RemoveIPC = mkDefault true;
+      RestrictNamespaces = mkDefault true;
+      RestrictRealtime = mkDefault true;
+      RestrictSUIDSGID = mkDefault true;
+      UMask = mkDefault "0066";
+      ProtectProc = mkDefault "invisible";
+      SystemCallFilter = mkBefore [
+        "~@clock"
+        "~@cpu-emulation"
+        "~@module"
+        "~@mount"
+        "~@obsolete"
+        "~@raw-io"
+        "~@reboot"
+        "~capset"
+        "~setdomainname"
+        "~sethostname"
+      ];
+      RestrictAddressFamilies = mkBefore [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ];
 
-    # Hardening (may overlap with DynamicUser=)
-    # The following options are only for optimizing:
-    # systemd-analyze security github-runner
-    AmbientCapabilities = "";
-    CapabilityBoundingSet = "";
-    # ProtectClock= adds DeviceAllow=char-rtc r
-    DeviceAllow = "";
-    NoNewPrivileges = true;
-    PrivateDevices = true;
-    PrivateMounts = true;
-    PrivateTmp = true;
-    PrivateUsers = true;
-    ProtectClock = true;
-    ProtectControlGroups = true;
-    ProtectHome = true;
-    ProtectHostname = true;
-    ProtectKernelLogs = true;
-    ProtectKernelModules = true;
-    ProtectKernelTunables = true;
-    ProtectSystem = "strict";
-    RemoveIPC = true;
-    RestrictNamespaces = true;
-    RestrictRealtime = true;
-    RestrictSUIDSGID = true;
-    UMask = "0066";
-    ProtectProc = "invisible";
-    SystemCallFilter = [
-      "~@clock"
-      "~@cpu-emulation"
-      "~@module"
-      "~@mount"
-      "~@obsolete"
-      "~@raw-io"
-      "~@reboot"
-      "~capset"
-      "~setdomainname"
-      "~sethostname"
-    ];
-    RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ];
+      BindPaths = lib.optionals (cfg.workDir != null) [ cfg.workDir ];
 
-    # Needs network access
-    PrivateNetwork = false;
-    # Cannot be true due to Node
-    MemoryDenyWriteExecute = false;
+      # Needs network access
+      PrivateNetwork = mkDefault false;
+      # Cannot be true due to Node
+      MemoryDenyWriteExecute = mkDefault false;
 
-    # The more restrictive "pid" option makes `nix` commands in CI emit
-    # "GC Warning: Couldn't read /proc/stat"
-    # You may want to set this to "pid" if not using `nix` commands
-    ProcSubset = "all";
-    # Coverage programs for compiled code such as `cargo-tarpaulin` disable
-    # ASLR (address space layout randomization) which requires the
-    # `personality` syscall
-    # You may want to set this to `true` if not using coverage tooling on
-    # compiled code
-    LockPersonality = false;
+      # The more restrictive "pid" option makes `nix` commands in CI emit
+      # "GC Warning: Couldn't read /proc/stat"
+      # You may want to set this to "pid" if not using `nix` commands
+      ProcSubset = mkDefault "all";
+      # Coverage programs for compiled code such as `cargo-tarpaulin` disable
+      # ASLR (address space layout randomization) which requires the
+      # `personality` syscall
+      # You may want to set this to `true` if not using coverage tooling on
+      # compiled code
+      LockPersonality = mkDefault false;
 
-    # Note that this has some interactions with the User setting; so you may
-    # want to consult the systemd docs if using both.
-    DynamicUser = true;
-  } // (
-    lib.optionalAttrs (cfg.user != null) { User = cfg.user; }
-  ) // cfg.serviceOverrides;
+      # Note that this has some interactions with the User setting; so you may
+      # want to consult the systemd docs if using both.
+      DynamicUser = mkDefault true;
+    }
+    (mkIf (cfg.user != null) { User = cfg.user; })
+    cfg.serviceOverrides
+  ];
 }
diff --git a/nixos/modules/services/continuous-integration/github-runners.nix b/nixos/modules/services/continuous-integration/github-runners.nix
index 78b57f9c7a2..66ace9580ec 100644
--- a/nixos/modules/services/continuous-integration/github-runners.nix
+++ b/nixos/modules/services/continuous-integration/github-runners.nix
@@ -53,4 +53,6 @@ in
         }))
     );
   };
+
+  meta.maintainers = with maintainers; [ veehaitch newam ];
 }
diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix
index 7b1c4da8626..10a2fe8a44d 100644
--- a/nixos/modules/services/continuous-integration/gitlab-runner.nix
+++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix
@@ -4,24 +4,41 @@ with lib;
 let
   cfg = config.services.gitlab-runner;
   hasDocker = config.virtualisation.docker.enable;
+
+  /* The whole logic of this module is to diff the hashes of the desired vs existing runners
+  The hash is recorded in the runner's name because we can't do better yet
+  See https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29350 for more details
+  */
+  genRunnerName = name: service: let
+      hash = substring 0 12 (hashString "md5" (unsafeDiscardStringContext (toJSON service)));
+    in if service ? description && service.description != null
+    then "${hash} ${service.description}"
+    else "${name}_${config.networking.hostName}_${hash}";
+
   hashedServices = mapAttrs'
-    (name: service: nameValuePair
-      "${name}_${config.networking.hostName}_${
-        substring 0 12
-        (hashString "md5" (unsafeDiscardStringContext (toJSON service)))}"
-      service)
-    cfg.services;
-  configPath = "$HOME/.gitlab-runner/config.toml";
-  configureScript = pkgs.writeShellScriptBin "gitlab-runner-configure" (
-    if (cfg.configFile != null) then ''
-      mkdir -p $(dirname ${configPath})
+    (name: service: nameValuePair (genRunnerName name service) service) cfg.services;
+  configPath = ''"$HOME"/.gitlab-runner/config.toml'';
+  configureScript = pkgs.writeShellApplication {
+    name = "gitlab-runner-configure";
+    runtimeInputs = with pkgs; [
+        bash
+        gawk
+        jq
+        moreutils
+        remarshal
+        util-linux
+        cfg.package
+        perl
+        python3
+    ];
+    text = if (cfg.configFile != null) then ''
       cp ${cfg.configFile} ${configPath}
       # make config file readable by service
-      chown -R --reference=$HOME $(dirname ${configPath})
+      chown -R --reference="$HOME" "$(dirname ${configPath})"
     '' else ''
       export CONFIG_FILE=${configPath}
 
-      mkdir -p $(dirname ${configPath})
+      mkdir -p "$(dirname ${configPath})"
       touch ${configPath}
 
       # update global options
@@ -34,22 +51,43 @@ let
       # remove no longer existing services
       gitlab-runner verify --delete
 
-      # current and desired state
-      NEEDED_SERVICES=$(echo ${concatStringsSep " " (attrNames hashedServices)} | tr " " "\n")
-      REGISTERED_SERVICES=$(gitlab-runner list 2>&1 | grep 'Executor' | awk '{ print $1 }')
+      ${toShellVar "NEEDED_SERVICES" (lib.mapAttrs (name: value: 1) hashedServices)}
+
+      declare -A REGISTERED_SERVICES
+
+      while IFS="," read -r name token;
+      do
+        REGISTERED_SERVICES["$name"]="$token"
+      done < <(gitlab-runner --log-format json list 2>&1 | grep Token  | jq -r '.msg +"," + .Token')
+
+      echo "NEEDED_SERVICES: " "''${!NEEDED_SERVICES[@]}"
+      echo "REGISTERED_SERVICES:" "''${!REGISTERED_SERVICES[@]}"
 
       # difference between current and desired state
-      NEW_SERVICES=$(grep -vxF -f <(echo "$REGISTERED_SERVICES") <(echo "$NEEDED_SERVICES") || true)
-      OLD_SERVICES=$(grep -vxF -f <(echo "$NEEDED_SERVICES") <(echo "$REGISTERED_SERVICES") || true)
+      declare -A NEW_SERVICES
+      for name in "''${!NEEDED_SERVICES[@]}"; do
+        if [ ! -v 'REGISTERED_SERVICES[$name]' ]; then
+          NEW_SERVICES[$name]=1
+        fi
+      done
+
+      declare -A OLD_SERVICES
+      # shellcheck disable=SC2034
+      for name in "''${!REGISTERED_SERVICES[@]}"; do
+        if [ ! -v 'NEEDED_SERVICES[$name]' ]; then
+          OLD_SERVICES[$name]=1
+        fi
+      done
 
       # register new services
       ${concatStringsSep "\n" (mapAttrsToList (name: service: ''
-        if echo "$NEW_SERVICES" | grep -xq "${name}"; then
+        # TODO so here we should mention NEW_SERVICES
+        if [ -v 'NEW_SERVICES["${name}"]' ] ; then
           bash -c ${escapeShellArg (concatStringsSep " \\\n " ([
             "set -a && source ${service.registrationConfigFile} &&"
             "gitlab-runner register"
             "--non-interactive"
-            (if service.description != null then "--description \"${service.description}\"" else "--name '${name}'")
+            "--name '${name}'"
             "--executor ${service.executor}"
             "--limit ${toString service.limit}"
             "--request-concurrency ${toString service.requestConcurrency}"
@@ -92,22 +130,26 @@ let
         fi
       '') hashedServices)}
 
+      # check key is in array https://stackoverflow.com/questions/30353951/how-to-check-if-dictionary-contains-a-key-in-bash
+
+      echo "NEW_SERVICES: ''${NEW_SERVICES[*]}"
+      echo "OLD_SERVICES: ''${OLD_SERVICES[*]}"
       # unregister old services
-      for NAME in $(echo "$OLD_SERVICES")
+      for NAME in "''${!OLD_SERVICES[@]}"
       do
-        [ ! -z "$NAME" ] && gitlab-runner unregister \
+        [ -n "$NAME" ] && gitlab-runner unregister \
           --name "$NAME" && sleep 1
       done
 
       # make config file readable by service
-      chown -R --reference=$HOME $(dirname ${configPath})
-    '');
+      chown -R --reference="$HOME" "$(dirname ${configPath})"
+    '';
+  };
   startScript = pkgs.writeShellScriptBin "gitlab-runner-start" ''
     export CONFIG_FILE=${configPath}
     exec gitlab-runner run --working-directory $HOME
   '';
-in
-{
+in {
   options.services.gitlab-runner = {
     enable = mkEnableOption (lib.mdDoc "Gitlab Runner");
     configFile = mkOption {
@@ -492,9 +534,9 @@ in
     };
   };
   config = mkIf cfg.enable {
-    warnings = (mapAttrsToList
+    warnings = mapAttrsToList
       (n: v: "services.gitlab-runner.services.${n}.`registrationConfigFile` points to a file in Nix Store. You should use quoted absolute path to prevent this.")
-      (filterAttrs (n: v: isStorePath v.registrationConfigFile) cfg.services));
+      (filterAttrs (n: v: isStorePath v.registrationConfigFile) cfg.services);
 
     environment.systemPackages = [ cfg.package ];
     systemd.services.gitlab-runner = {
@@ -528,14 +570,14 @@ in
         ExecStartPre = "!${configureScript}/bin/gitlab-runner-configure";
         ExecStart = "${startScript}/bin/gitlab-runner-start";
         ExecReload = "!${configureScript}/bin/gitlab-runner-configure";
-      } // optionalAttrs (cfg.gracefulTermination) {
+      } // optionalAttrs cfg.gracefulTermination {
         TimeoutStopSec = "${cfg.gracefulTimeout}";
         KillSignal = "SIGQUIT";
         KillMode = "process";
       };
     };
     # Enable periodic clear-docker-cache script
-    systemd.services.gitlab-runner-clear-docker-cache = {
+    systemd.services.gitlab-runner-clear-docker-cache = mkIf (cfg.clear-docker-cache.enable && (any (s: s.executor == "docker") (attrValues cfg.services))) {
       description = "Prune gitlab-runner docker resources";
       restartIfChanged = false;
       unitConfig.X-StopOnRemoval = false;
@@ -548,7 +590,7 @@ in
         ${pkgs.gitlab-runner}/bin/clear-docker-cache ${toString cfg.clear-docker-cache.flags}
       '';
 
-      startAt = optional cfg.clear-docker-cache.enable cfg.clear-docker-cache.dates;
+      startAt = cfg.clear-docker-cache.dates;
     };
     # Enable docker if `docker` executor is used in any service
     virtualisation.docker.enable = mkIf (
@@ -569,4 +611,6 @@ in
     (mkRenamedOptionModule [ "services" "gitlab-runner" "sessionServer" "advertiseAddress" ] [ "services" "gitlab-runner" "settings" "session_server" "advertise_address" ] )
     (mkRenamedOptionModule [ "services" "gitlab-runner" "sessionServer" "sessionTimeout" ] [ "services" "gitlab-runner" "settings" "session_server" "session_timeout" ] )
   ];
+
+  meta.maintainers = teams.gitlab.members;
 }
diff --git a/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
index 663f3df775c..ea9b5ffbf43 100644
--- a/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
@@ -10,171 +10,18 @@
 let
   inherit (lib)
     filterAttrs
-    literalMD
     literalExpression
     mkIf
     mkOption
     mkRemovedOptionModule
     mkRenamedOptionModule
     types
-    ;
-
-  cfg =
-    config.services.hercules-ci-agent;
-
-  format = pkgs.formats.toml { };
-
-  settingsModule = { config, ... }: {
-    freeformType = format.type;
-    options = {
-      apiBaseUrl = mkOption {
-        description = lib.mdDoc ''
-          API base URL that the agent will connect to.
-
-          When using Hercules CI Enterprise, set this to the URL where your
-          Hercules CI server is reachable.
-        '';
-        type = types.str;
-        default = "https://hercules-ci.com";
-      };
-      baseDirectory = mkOption {
-        type = types.path;
-        default = "/var/lib/hercules-ci-agent";
-        description = lib.mdDoc ''
-          State directory (secrets, work directory, etc) for agent
-        '';
-      };
-      concurrentTasks = mkOption {
-        description = lib.mdDoc ''
-          Number of tasks to perform simultaneously.
-
-          A task is a single derivation build, an evaluation or an effect run.
-          At minimum, you need 2 concurrent tasks for `x86_64-linux`
-          in your cluster, to allow for import from derivation.
-
-          `concurrentTasks` can be around the CPU core count or lower if memory is
-          the bottleneck.
-
-          The optimal value depends on the resource consumption characteristics of your workload,
-          including memory usage and in-task parallelism. This is typically determined empirically.
-
-          When scaling, it is generally better to have a double-size machine than two machines,
-          because each split of resources causes inefficiencies; particularly with regards
-          to build latency because of extra downloads.
-        '';
-        type = types.either types.ints.positive (types.enum [ "auto" ]);
-        default = "auto";
-      };
-      labels = mkOption {
-        description = lib.mdDoc ''
-          A key-value map of user data.
-
-          This data will be available to organization members in the dashboard and API.
-
-          The values can be of any TOML type that corresponds to a JSON type, but arrays
-          can not contain tables/objects due to limitations of the TOML library. Values
-          involving arrays of non-primitive types may not be representable currently.
-        '';
-        type = format.type;
-        defaultText = literalExpression ''
-          {
-            agent.source = "..."; # One of "nixpkgs", "flake", "override"
-            lib.version = "...";
-            pkgs.version = "...";
-          }
-        '';
-      };
-      workDirectory = mkOption {
-        description = lib.mdDoc ''
-          The directory in which temporary subdirectories are created for task state. This includes sources for Nix evaluation.
-        '';
-        type = types.path;
-        default = config.baseDirectory + "/work";
-        defaultText = literalExpression ''baseDirectory + "/work"'';
-      };
-      staticSecretsDirectory = mkOption {
-        description = lib.mdDoc ''
-          This is the default directory to look for statically configured secrets like `cluster-join-token.key`.
-
-          See also `clusterJoinTokenPath` and `binaryCachesPath` for fine-grained configuration.
-        '';
-        type = types.path;
-        default = config.baseDirectory + "/secrets";
-        defaultText = literalExpression ''baseDirectory + "/secrets"'';
-      };
-      clusterJoinTokenPath = mkOption {
-        description = lib.mdDoc ''
-          Location of the cluster-join-token.key file.
 
-          You can retrieve the contents of the file when creating a new agent via
-          <https://hercules-ci.com/dashboard>.
-
-          As this value is confidential, it should not be in the store, but
-          installed using other means, such as agenix, NixOps
-          `deployment.keys`, or manual installation.
-
-          The contents of the file are used for authentication between the agent and the API.
-        '';
-        type = types.path;
-        default = config.staticSecretsDirectory + "/cluster-join-token.key";
-        defaultText = literalExpression ''staticSecretsDirectory + "/cluster-join-token.key"'';
-      };
-      binaryCachesPath = mkOption {
-        description = lib.mdDoc ''
-          Path to a JSON file containing binary cache secret keys.
-
-          As these values are confidential, they should not be in the store, but
-          copied over using other means, such as agenix, NixOps
-          `deployment.keys`, or manual installation.
-
-          The format is described on <https://docs.hercules-ci.com/hercules-ci-agent/binary-caches-json/>.
-        '';
-        type = types.path;
-        default = config.staticSecretsDirectory + "/binary-caches.json";
-        defaultText = literalExpression ''staticSecretsDirectory + "/binary-caches.json"'';
-      };
-      secretsJsonPath = mkOption {
-        description = lib.mdDoc ''
-          Path to a JSON file containing secrets for effects.
-
-          As these values are confidential, they should not be in the store, but
-          copied over using other means, such as agenix, NixOps
-          `deployment.keys`, or manual installation.
-
-          The format is described on <https://docs.hercules-ci.com/hercules-ci-agent/secrets-json/>.
-        '';
-        type = types.path;
-        default = config.staticSecretsDirectory + "/secrets.json";
-        defaultText = literalExpression ''staticSecretsDirectory + "/secrets.json"'';
-      };
-    };
-  };
+    ;
 
-  # TODO (roberth, >=2022) remove
-  checkNix =
-    if !cfg.checkNix
-    then ""
-    else if lib.versionAtLeast config.nix.package.version "2.3.10"
-    then ""
-    else
-      pkgs.stdenv.mkDerivation {
-        name = "hercules-ci-check-system-nix-src";
-        inherit (config.nix.package) src patches;
-        dontConfigure = true;
-        buildPhase = ''
-          echo "Checking in-memory pathInfoCache expiry"
-          if ! grep 'PathInfoCacheValue' src/libstore/store-api.hh >/dev/null; then
-            cat 1>&2 <<EOF
+  cfg = config.services.hercules-ci-agent;
 
-            You are deploying Hercules CI Agent on a system with an incompatible
-            nix-daemon. Please make sure nix.package is set to a Nix version of at
-            least 2.3.10 or a master version more recent than Mar 12, 2020.
-          EOF
-            exit 1
-          fi
-        '';
-        installPhase = "touch $out";
-      };
+  inherit (import ./settings.nix { inherit pkgs lib; }) format settingsModule;
 
 in
 {
@@ -198,15 +45,6 @@ in
         Support is available at [help@hercules-ci.com](mailto:help@hercules-ci.com).
       '';
     };
-    checkNix = mkOption {
-      type = types.bool;
-      default = true;
-      description = lib.mdDoc ''
-        Whether to make sure that the system's Nix (nix-daemon) is compatible.
-
-        If you set this to false, please keep up with the change log.
-      '';
-    };
     package = mkOption {
       description = lib.mdDoc ''
         Package containing the bin/hercules-ci-agent executable.
@@ -235,7 +73,7 @@ in
     tomlFile = mkOption {
       type = types.path;
       internal = true;
-      defaultText = literalMD "generated `hercules-ci-agent.toml`";
+      defaultText = lib.literalMD "generated `hercules-ci-agent.toml`";
       description = lib.mdDoc ''
         The fully assembled config file.
       '';
@@ -243,7 +81,27 @@ in
   };
 
   config = mkIf cfg.enable {
-    nix.extraOptions = lib.addContextFrom checkNix ''
+    # Make sure that nix.extraOptions does not override trusted-users
+    assertions = [
+      {
+        assertion =
+          (cfg.settings.nixUserIsTrusted or false) ->
+          builtins.match ".*(^|\n)[ \t]*trusted-users[ \t]*=.*" config.nix.extraOptions == null;
+        message = ''
+          hercules-ci-agent: Please do not set `trusted-users` in `nix.extraOptions`.
+
+          The hercules-ci-agent module by default relies on `nix.settings.trusted-users`
+          to be effectful, but a line like `trusted-users = ...` in `nix.extraOptions`
+          will override the value set in `nix.settings.trusted-users`.
+
+          Instead of setting `trusted-users` in the `nix.extraOptions` string, you should
+          set an option with additive semantics, such as
+           - the NixOS option `nix.settings.trusted-users`, or
+           - the Nix option in the `extraOptions` string, `extra-trusted-users`
+        '';
+      }
+    ];
+    nix.extraOptions = ''
       # A store path that was missing at first may well have finished building,
       # even shortly after the previous lookup. This *also* applies to the daemon.
       narinfo-cache-negative-ttl = 0
@@ -251,14 +109,9 @@ in
     services.hercules-ci-agent = {
       tomlFile =
         format.generate "hercules-ci-agent.toml" cfg.settings;
-
-      settings.labels = {
-        agent.source =
-          if options.services.hercules-ci-agent.package.highestPrio == (lib.modules.mkOptionDefault { }).priority
-          then "nixpkgs"
-          else lib.mkOptionDefault "override";
-        pkgs.version = pkgs.lib.version;
-        lib.version = lib.version;
+      settings.config._module.args = {
+        packageOption = options.services.hercules-ci-agent.package;
+        inherit pkgs;
       };
     };
   };
diff --git a/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
index ef1933e1228..ad26b5316dd 100644
--- a/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
@@ -35,6 +35,15 @@ in
         ExecStartPre = testCommand;
         Restart = "on-failure";
         RestartSec = 120;
+
+        # If a worker goes OOM, don't kill the main process. It needs to
+        # report the failure and it's unlikely to be part of the problem.
+        OOMPolicy = "continue";
+
+        # Work around excessive stack use by libstdc++ regex
+        # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=86164
+        # A 256 MiB stack allows between 400 KiB and 1.5 MiB file to be matched by ".*".
+        LimitSTACK = 256 * 1024 * 1024;
       };
     };
 
diff --git a/nixos/modules/services/continuous-integration/hercules-ci-agent/settings.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/settings.nix
new file mode 100644
index 00000000000..8eb902313ee
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/settings.nix
@@ -0,0 +1,153 @@
+# Not a module
+{ pkgs, lib }:
+let
+  inherit (lib)
+    types
+    literalExpression
+    mkOption
+    ;
+
+  format = pkgs.formats.toml { };
+
+  settingsModule = { config, packageOption, pkgs, ... }: {
+    freeformType = format.type;
+    options = {
+      apiBaseUrl = mkOption {
+        description = lib.mdDoc ''
+          API base URL that the agent will connect to.
+
+          When using Hercules CI Enterprise, set this to the URL where your
+          Hercules CI server is reachable.
+        '';
+        type = types.str;
+        default = "https://hercules-ci.com";
+      };
+      baseDirectory = mkOption {
+        type = types.path;
+        default = "/var/lib/hercules-ci-agent";
+        description = lib.mdDoc ''
+          State directory (secrets, work directory, etc) for agent
+        '';
+      };
+      concurrentTasks = mkOption {
+        description = lib.mdDoc ''
+          Number of tasks to perform simultaneously.
+
+          A task is a single derivation build, an evaluation or an effect run.
+          At minimum, you need 2 concurrent tasks for `x86_64-linux`
+          in your cluster, to allow for import from derivation.
+
+          `concurrentTasks` can be around the CPU core count or lower if memory is
+          the bottleneck.
+
+          The optimal value depends on the resource consumption characteristics of your workload,
+          including memory usage and in-task parallelism. This is typically determined empirically.
+
+          When scaling, it is generally better to have a double-size machine than two machines,
+          because each split of resources causes inefficiencies; particularly with regards
+          to build latency because of extra downloads.
+        '';
+        type = types.either types.ints.positive (types.enum [ "auto" ]);
+        default = "auto";
+        defaultText = lib.literalMD ''
+          `"auto"`, meaning equal to the number of CPU cores.
+        '';
+      };
+      labels = mkOption {
+        description = lib.mdDoc ''
+          A key-value map of user data.
+
+          This data will be available to organization members in the dashboard and API.
+
+          The values can be of any TOML type that corresponds to a JSON type, but arrays
+          can not contain tables/objects due to limitations of the TOML library. Values
+          involving arrays of non-primitive types may not be representable currently.
+        '';
+        type = format.type;
+        defaultText = literalExpression ''
+          {
+            agent.source = "..."; # One of "nixpkgs", "flake", "override"
+            lib.version = "...";
+            pkgs.version = "...";
+          }
+        '';
+      };
+      workDirectory = mkOption {
+        description = lib.mdDoc ''
+          The directory in which temporary subdirectories are created for task state. This includes sources for Nix evaluation.
+        '';
+        type = types.path;
+        default = config.baseDirectory + "/work";
+        defaultText = literalExpression ''baseDirectory + "/work"'';
+      };
+      staticSecretsDirectory = mkOption {
+        description = lib.mdDoc ''
+          This is the default directory to look for statically configured secrets like `cluster-join-token.key`.
+
+          See also `clusterJoinTokenPath` and `binaryCachesPath` for fine-grained configuration.
+        '';
+        type = types.path;
+        default = config.baseDirectory + "/secrets";
+        defaultText = literalExpression ''baseDirectory + "/secrets"'';
+      };
+      clusterJoinTokenPath = mkOption {
+        description = lib.mdDoc ''
+          Location of the cluster-join-token.key file.
+
+          You can retrieve the contents of the file when creating a new agent via
+          <https://hercules-ci.com/dashboard>.
+
+          As this value is confidential, it should not be in the store, but
+          installed using other means, such as agenix, NixOps
+          `deployment.keys`, or manual installation.
+
+          The contents of the file are used for authentication between the agent and the API.
+        '';
+        type = types.path;
+        default = config.staticSecretsDirectory + "/cluster-join-token.key";
+        defaultText = literalExpression ''staticSecretsDirectory + "/cluster-join-token.key"'';
+      };
+      binaryCachesPath = mkOption {
+        description = lib.mdDoc ''
+          Path to a JSON file containing binary cache secret keys.
+
+          As these values are confidential, they should not be in the store, but
+          copied over using other means, such as agenix, NixOps
+          `deployment.keys`, or manual installation.
+
+          The format is described on <https://docs.hercules-ci.com/hercules-ci-agent/binary-caches-json/>.
+        '';
+        type = types.path;
+        default = config.staticSecretsDirectory + "/binary-caches.json";
+        defaultText = literalExpression ''staticSecretsDirectory + "/binary-caches.json"'';
+      };
+      secretsJsonPath = mkOption {
+        description = lib.mdDoc ''
+          Path to a JSON file containing secrets for effects.
+
+          As these values are confidential, they should not be in the store, but
+          copied over using other means, such as agenix, NixOps
+          `deployment.keys`, or manual installation.
+
+          The format is described on <https://docs.hercules-ci.com/hercules-ci-agent/secrets-json/>.
+        '';
+        type = types.path;
+        default = config.staticSecretsDirectory + "/secrets.json";
+        defaultText = literalExpression ''staticSecretsDirectory + "/secrets.json"'';
+      };
+    };
+    config = {
+      labels = {
+        agent.source =
+          if packageOption.highestPrio == (lib.modules.mkOptionDefault { }).priority
+          then "nixpkgs"
+          else lib.mkOptionDefault "override";
+        pkgs.version = pkgs.lib.version;
+        lib.version = lib.version;
+      };
+    };
+  };
+in
+{
+  inherit format settingsModule;
+}
diff --git a/nixos/modules/services/continuous-integration/hydra/default.nix b/nixos/modules/services/continuous-integration/hydra/default.nix
index 564bcd37dec..83078706fca 100644
--- a/nixos/modules/services/continuous-integration/hydra/default.nix
+++ b/nixos/modules/services/continuous-integration/hydra/default.nix
@@ -398,7 +398,7 @@ in
     systemd.services.hydra-evaluator =
       { wantedBy = [ "multi-user.target" ];
         requires = [ "hydra-init.service" ];
-        after = [ "hydra-init.service" "network.target" ];
+        after = [ "hydra-init.service" "network.target" "network-online.target" ];
         path = with pkgs; [ hydra-package nettools jq ];
         restartTriggers = [ hydraConf ];
         environment = env // {
diff --git a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
index 3a1c6c1a371..a8e3effd1f7 100644
--- a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
@@ -9,25 +9,20 @@ let
 in {
   options = {
     services.jenkins.jobBuilder = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether or not to enable the Jenkins Job Builder (JJB) service. It
-          allows defining jobs for Jenkins in a declarative manner.
+      enable = mkEnableOption (mdDoc ''
+        the Jenkins Job Builder (JJB) service. It
+        allows defining jobs for Jenkins in a declarative manner.
 
-          Jobs managed through the Jenkins WebUI (or by other means) are left
-          unchanged.
+        Jobs managed through the Jenkins WebUI (or by other means) are left
+        unchanged.
 
-          Note that it really is declarative configuration; if you remove a
-          previously defined job, the corresponding job directory will be
-          deleted.
+        Note that it really is declarative configuration; if you remove a
+        previously defined job, the corresponding job directory will be
+        deleted.
 
-          Please see the Jenkins Job Builder documentation for more info:
-          [
-          http://docs.openstack.org/infra/jenkins-job-builder/](http://docs.openstack.org/infra/jenkins-job-builder/)
-        '';
-      };
+        Please see the Jenkins Job Builder documentation for more info:
+        <https://jenkins-job-builder.readthedocs.io/>
+      '');
 
       accessUser = mkOption {
         default = "admin";
@@ -242,7 +237,7 @@ in {
                 jobdir="${jenkinsCfg.home}/$jenkinsjobname"
                 rm -rf "$jobdir"
             done
-          '' + (if cfg.accessUser != "" then reloadScript else "");
+          '' + (optionalString (cfg.accessUser != "") reloadScript);
       serviceConfig = {
         Type = "oneshot";
         User = jenkinsCfg.user;
diff --git a/nixos/modules/services/continuous-integration/woodpecker/agents.nix b/nixos/modules/services/continuous-integration/woodpecker/agents.nix
new file mode 100644
index 00000000000..cc5b903afd5
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/woodpecker/agents.nix
@@ -0,0 +1,144 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.services.woodpecker-agents;
+
+  agentModule = lib.types.submodule {
+    options = {
+      enable = lib.mkEnableOption (lib.mdDoc "this Woodpecker-Agent. Agents execute tasks generated by a Server, every install will need one server and at least one agent");
+
+      package = lib.mkPackageOptionMD pkgs "woodpecker-agent" { };
+
+      environment = lib.mkOption {
+        default = { };
+        type = lib.types.attrsOf lib.types.str;
+        example = lib.literalExpression ''
+          {
+            WOODPECKER_SERVER = "localhost:9000";
+            WOODPECKER_BACKEND = "docker";
+            DOCKER_HOST = "unix:///run/podman/podman.sock";
+          }
+        '';
+        description = lib.mdDoc "woodpecker-agent config environment variables, for other options read the [documentation](https://woodpecker-ci.org/docs/administration/agent-config)";
+      };
+
+      extraGroups = lib.mkOption {
+        type = lib.types.listOf lib.types.str;
+        default = [ ];
+        example = [ "podman" ];
+        description = lib.mdDoc ''
+          Additional groups for the systemd service.
+        '';
+      };
+
+      environmentFile = lib.mkOption {
+        type = lib.types.listOf lib.types.path;
+        default = [ ];
+        example = [ "/var/secrets/woodpecker-agent.env" ];
+        description = lib.mdDoc ''
+          File to load environment variables
+          from. This is helpful for specifying secrets.
+          Example content of environmentFile:
+          ```
+          WOODPECKER_AGENT_SECRET=your-shared-secret-goes-here
+          ```
+        '';
+      };
+    };
+  };
+
+  mkAgentService = name: agentCfg: {
+    name = "woodpecker-agent-${name}";
+    value = {
+      description = "Woodpecker-Agent Service - ${name}";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        SupplementaryGroups = agentCfg.extraGroups;
+        EnvironmentFile = agentCfg.environmentFile;
+        ExecStart = lib.getExe agentCfg.package;
+        Restart = "on-failure";
+        RestartSec = 15;
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@clock @privileged @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @reboot @setuid @swap";
+        BindReadOnlyPaths = [
+          "-/etc/resolv.conf"
+          "-/etc/nsswitch.conf"
+          "-/etc/ssl/certs"
+          "-/etc/static/ssl/certs"
+          "-/etc/hosts"
+          "-/etc/localtime"
+        ];
+      };
+      inherit (agentCfg) environment;
+    };
+  };
+in
+{
+  meta.maintainers = with lib.maintainers; [ janik ambroisie ];
+
+  options = {
+    services.woodpecker-agents = {
+      agents = lib.mkOption {
+        default = { };
+        type = lib.types.attrsOf agentModule;
+        example = {
+          docker = {
+            environment = {
+              WOODPECKER_SERVER = "localhost:9000";
+              WOODPECKER_BACKEND = "docker";
+              DOCKER_HOST = "unix:///run/podman/podman.sock";
+            };
+
+            extraGroups = [ "docker" ];
+
+            environmentFile = "/run/secrets/woodpecker/agent-secret.txt";
+          };
+
+          exec = {
+            environment = {
+              WOODPECKER_SERVER = "localhost:9000";
+              WOODPECKER_BACKEND = "exec";
+            };
+
+            environmentFile = "/run/secrets/woodpecker/agent-secret.txt";
+          };
+        };
+        description = lib.mdDoc "woodpecker-agents configurations";
+      };
+    };
+  };
+
+  config = {
+    systemd.services =
+      let
+        mkServices = lib.mapAttrs' mkAgentService;
+        enabledAgents = lib.filterAttrs (_: agent: agent.enable) cfg.agents;
+      in
+      mkServices enabledAgents;
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/woodpecker/server.nix b/nixos/modules/services/continuous-integration/woodpecker/server.nix
new file mode 100644
index 00000000000..cae5ed7cf11
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/woodpecker/server.nix
@@ -0,0 +1,98 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.services.woodpecker-server;
+in
+{
+  meta.maintainers = with lib.maintainers; [ janik ambroisie ];
+
+
+  options = {
+    services.woodpecker-server = {
+      enable = lib.mkEnableOption (lib.mdDoc "the Woodpecker-Server, a CI/CD application for automatic builds, deployments and tests");
+      package = lib.mkPackageOptionMD pkgs "woodpecker-server" { };
+      environment = lib.mkOption {
+        default = { };
+        type = lib.types.attrsOf lib.types.str;
+        example = lib.literalExpression
+          ''
+            {
+              WOODPECKER_HOST = "https://woodpecker.example.com";
+              WOODPECKER_OPEN = "true";
+              WOODPECKER_GITEA = "true";
+              WOODPECKER_GITEA_CLIENT = "ffffffff-ffff-ffff-ffff-ffffffffffff";
+              WOODPECKER_GITEA_URL = "https://git.example.com";
+            }
+          '';
+        description = lib.mdDoc "woodpecker-server config environment variables, for other options read the [documentation](https://woodpecker-ci.org/docs/administration/server-config)";
+      };
+      environmentFile = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        example = "/root/woodpecker-server.env";
+        description = lib.mdDoc ''
+          File to load environment variables
+          from. This is helpful for specifying secrets.
+          Example content of environmentFile:
+          ```
+          WOODPECKER_AGENT_SECRET=your-shared-secret-goes-here
+          WOODPECKER_GITEA_SECRET=gto_**************************************
+          ```
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services = {
+      woodpecker-server = {
+        description = "Woodpecker-Server Service";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network-online.target" ];
+        wants = [ "network-online.target" ];
+        serviceConfig = {
+          DynamicUser = true;
+          WorkingDirectory = "%S/woodpecker-server";
+          StateDirectory = "woodpecker-server";
+          StateDirectoryMode = "0700";
+          UMask = "0007";
+          ConfigurationDirectory = "woodpecker-server";
+          EnvironmentFile = lib.optional (cfg.environmentFile != null) cfg.environmentFile;
+          ExecStart = "${cfg.package}/bin/woodpecker-server";
+          Restart = "on-failure";
+          RestartSec = 15;
+          CapabilityBoundingSet = "";
+          # Security
+          NoNewPrivileges = true;
+          # Sandboxing
+          ProtectSystem = "strict";
+          ProtectHome = true;
+          PrivateTmp = true;
+          PrivateDevices = true;
+          PrivateUsers = true;
+          ProtectHostname = true;
+          ProtectClock = true;
+          ProtectKernelTunables = true;
+          ProtectKernelModules = true;
+          ProtectKernelLogs = true;
+          ProtectControlGroups = true;
+          RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ];
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          PrivateMounts = true;
+          # System Call Filtering
+          SystemCallArchitectures = "native";
+          SystemCallFilter = "~@clock @privileged @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @reboot @setuid @swap";
+        };
+        inherit (cfg) environment;
+      };
+    };
+  };
+}
+
diff --git a/nixos/modules/services/databases/clickhouse.nix b/nixos/modules/services/databases/clickhouse.nix
index 04dd20b5f14..dca352ef72f 100644
--- a/nixos/modules/services/databases/clickhouse.nix
+++ b/nixos/modules/services/databases/clickhouse.nix
@@ -48,13 +48,20 @@ with lib;
       after = [ "network.target" ];
 
       serviceConfig = {
+        Type = "notify";
         User = "clickhouse";
         Group = "clickhouse";
         ConfigurationDirectory = "clickhouse-server";
         AmbientCapabilities = "CAP_SYS_NICE";
         StateDirectory = "clickhouse";
         LogsDirectory = "clickhouse";
-        ExecStart = "${cfg.package}/bin/clickhouse-server --config-file=${cfg.package}/etc/clickhouse-server/config.xml";
+        ExecStart = "${cfg.package}/bin/clickhouse-server --config-file=/etc/clickhouse-server/config.xml";
+        TimeoutStartSec = "infinity";
+      };
+
+      environment = {
+        # Switching off watchdog is very important for sd_notify to work correctly.
+        CLICKHOUSE_WATCHDOG_ENABLE = "0";
       };
     };
 
diff --git a/nixos/modules/services/databases/cockroachdb.nix b/nixos/modules/services/databases/cockroachdb.nix
index 26ccb030b3d..ff77d30588f 100644
--- a/nixos/modules/services/databases/cockroachdb.nix
+++ b/nixos/modules/services/databases/cockroachdb.nix
@@ -164,7 +164,7 @@ in
         example = [ "--advertise-addr" "[fe80::f6f2:::]" ];
         description = lib.mdDoc ''
           Extra CLI arguments passed to {command}`cockroach start`.
-          For the full list of supported argumemnts, check <https://www.cockroachlabs.com/docs/stable/cockroach-start.html#flags>
+          For the full list of supported arguments, check <https://www.cockroachlabs.com/docs/stable/cockroach-start.html#flags>
         '';
       };
     };
diff --git a/nixos/modules/services/databases/couchdb.nix b/nixos/modules/services/databases/couchdb.nix
index cdf32654e66..0a81a8dceee 100644
--- a/nixos/modules/services/databases/couchdb.nix
+++ b/nixos/modules/services/databases/couchdb.nix
@@ -141,7 +141,7 @@ in {
         type = types.lines;
         default = "";
         description = lib.mdDoc ''
-          Extra configuration. Overrides any other cofiguration.
+          Extra configuration. Overrides any other configuration.
         '';
       };
 
diff --git a/nixos/modules/services/databases/dgraph.nix b/nixos/modules/services/databases/dgraph.nix
index 5726851a43f..7f005a9971a 100644
--- a/nixos/modules/services/databases/dgraph.nix
+++ b/nixos/modules/services/databases/dgraph.nix
@@ -12,7 +12,7 @@ let
   ''
     mkdir -p $out/bin
     makeWrapper ${cfg.package}/bin/dgraph $out/bin/dgraph \
-      --set PATH '${lib.makeBinPath [ pkgs.nodejs ]}:$PATH' \
+      --prefix PATH : "${lib.makeBinPath [ pkgs.nodejs ]}" \
   '';
   securityOptions = {
       NoNewPrivileges = true;
@@ -55,7 +55,7 @@ in
     services.dgraph = {
       enable = mkEnableOption (lib.mdDoc "Dgraph native GraphQL database with a graph backend");
 
-      package = lib.mkPackageOption pkgs "dgraph" { };
+      package = lib.mkPackageOptionMD pkgs "dgraph" { };
 
       settings = mkOption {
         type = settingsFormat.type;
diff --git a/nixos/modules/services/databases/firebird.nix b/nixos/modules/services/databases/firebird.nix
index 4c285534536..26ed46f0e60 100644
--- a/nixos/modules/services/databases/firebird.nix
+++ b/nixos/modules/services/databases/firebird.nix
@@ -1,6 +1,6 @@
 { config, lib, pkgs, ... }:
 
-# TODO: This may file may need additional review, eg which configuartions to
+# TODO: This may file may need additional review, eg which configurations to
 # expose to the user.
 #
 # I only used it to access some simple databases.
diff --git a/nixos/modules/services/databases/foundationdb.md b/nixos/modules/services/databases/foundationdb.md
new file mode 100644
index 00000000000..0815c139152
--- /dev/null
+++ b/nixos/modules/services/databases/foundationdb.md
@@ -0,0 +1,309 @@
+# FoundationDB {#module-services-foundationdb}
+
+*Source:* {file}`modules/services/databases/foundationdb.nix`
+
+*Upstream documentation:* <https://apple.github.io/foundationdb/>
+
+*Maintainer:* Austin Seipp
+
+*Available version(s):* 7.1.x
+
+FoundationDB (or "FDB") is an open source, distributed, transactional
+key-value store.
+
+## Configuring and basic setup {#module-services-foundationdb-configuring}
+
+To enable FoundationDB, add the following to your
+{file}`configuration.nix`:
+```
+services.foundationdb.enable = true;
+services.foundationdb.package = pkgs.foundationdb71; # FoundationDB 7.1.x
+```
+
+The {option}`services.foundationdb.package` option is required, and
+must always be specified. Due to the fact FoundationDB network protocols and
+on-disk storage formats may change between (major) versions, and upgrades
+must be explicitly handled by the user, you must always manually specify
+this yourself so that the NixOS module will use the proper version. Note
+that minor, bugfix releases are always compatible.
+
+After running {command}`nixos-rebuild`, you can verify whether
+FoundationDB is running by executing {command}`fdbcli` (which is
+added to {option}`environment.systemPackages`):
+```ShellSession
+$ sudo -u foundationdb fdbcli
+Using cluster file `/etc/foundationdb/fdb.cluster'.
+
+The database is available.
+
+Welcome to the fdbcli. For help, type `help'.
+fdb> status
+
+Using cluster file `/etc/foundationdb/fdb.cluster'.
+
+Configuration:
+  Redundancy mode        - single
+  Storage engine         - memory
+  Coordinators           - 1
+
+Cluster:
+  FoundationDB processes - 1
+  Machines               - 1
+  Memory availability    - 5.4 GB per process on machine with least available
+  Fault Tolerance        - 0 machines
+  Server time            - 04/20/18 15:21:14
+
+...
+
+fdb>
+```
+
+You can also write programs using the available client libraries. For
+example, the following Python program can be run in order to grab the
+cluster status, as a quick example. (This example uses
+{command}`nix-shell` shebang support to automatically supply the
+necessary Python modules).
+```ShellSession
+a@link> cat fdb-status.py
+#! /usr/bin/env nix-shell
+#! nix-shell -i python -p python pythonPackages.foundationdb71
+
+import fdb
+import json
+
+def main():
+    fdb.api_version(520)
+    db = fdb.open()
+
+    @fdb.transactional
+    def get_status(tr):
+        return str(tr['\xff\xff/status/json'])
+
+    obj = json.loads(get_status(db))
+    print('FoundationDB available: %s' % obj['client']['database_status']['available'])
+
+if __name__ == "__main__":
+    main()
+a@link> chmod +x fdb-status.py
+a@link> ./fdb-status.py
+FoundationDB available: True
+a@link>
+```
+
+FoundationDB is run under the {command}`foundationdb` user and group
+by default, but this may be changed in the NixOS configuration. The systemd
+unit {command}`foundationdb.service` controls the
+{command}`fdbmonitor` process.
+
+By default, the NixOS module for FoundationDB creates a single SSD-storage
+based database for development and basic usage. This storage engine is
+designed for SSDs and will perform poorly on HDDs; however it can handle far
+more data than the alternative "memory" engine and is a better default
+choice for most deployments. (Note that you can change the storage backend
+on-the-fly for a given FoundationDB cluster using
+{command}`fdbcli`.)
+
+Furthermore, only 1 server process and 1 backup agent are started in the
+default configuration. See below for more on scaling to increase this.
+
+FoundationDB stores all data for all server processes under
+{file}`/var/lib/foundationdb`. You can override this using
+{option}`services.foundationdb.dataDir`, e.g.
+```
+services.foundationdb.dataDir = "/data/fdb";
+```
+
+Similarly, logs are stored under {file}`/var/log/foundationdb`
+by default, and there is a corresponding
+{option}`services.foundationdb.logDir` as well.
+
+## Scaling processes and backup agents {#module-services-foundationdb-scaling}
+
+Scaling the number of server processes is quite easy; simply specify
+{option}`services.foundationdb.serverProcesses` to be the number of
+FoundationDB worker processes that should be started on the machine.
+
+FoundationDB worker processes typically require 4GB of RAM per-process at
+minimum for good performance, so this option is set to 1 by default since
+the maximum amount of RAM is unknown. You're advised to abide by this
+restriction, so pick a number of processes so that each has 4GB or more.
+
+A similar option exists in order to scale backup agent processes,
+{option}`services.foundationdb.backupProcesses`. Backup agents are
+not as performance/RAM sensitive, so feel free to experiment with the number
+of available backup processes.
+
+## Clustering {#module-services-foundationdb-clustering}
+
+FoundationDB on NixOS works similarly to other Linux systems, so this
+section will be brief. Please refer to the full FoundationDB documentation
+for more on clustering.
+
+FoundationDB organizes clusters using a set of
+*coordinators*, which are just specially-designated
+worker processes. By default, every installation of FoundationDB on NixOS
+will start as its own individual cluster, with a single coordinator: the
+first worker process on {command}`localhost`.
+
+Coordinators are specified globally using the
+{command}`/etc/foundationdb/fdb.cluster` file, which all servers and
+client applications will use to find and join coordinators. Note that this
+file *can not* be managed by NixOS so easily:
+FoundationDB is designed so that it will rewrite the file at runtime for all
+clients and nodes when cluster coordinators change, with clients
+transparently handling this without intervention. It is fundamentally a
+mutable file, and you should not try to manage it in any way in NixOS.
+
+When dealing with a cluster, there are two main things you want to do:
+
+  - Add a node to the cluster for storage/compute.
+  - Promote an ordinary worker to a coordinator.
+
+A node must already be a member of the cluster in order to properly be
+promoted to a coordinator, so you must always add it first if you wish to
+promote it.
+
+To add a machine to a FoundationDB cluster:
+
+  - Choose one of the servers to start as the initial coordinator.
+  - Copy the {command}`/etc/foundationdb/fdb.cluster` file from this
+    server to all the other servers. Restart FoundationDB on all of these
+    other servers, so they join the cluster.
+  - All of these servers are now connected and working together in the
+    cluster, under the chosen coordinator.
+
+At this point, you can add as many nodes as you want by just repeating the
+above steps. By default there will still be a single coordinator: you can
+use {command}`fdbcli` to change this and add new coordinators.
+
+As a convenience, FoundationDB can automatically assign coordinators based
+on the redundancy mode you wish to achieve for the cluster. Once all the
+nodes have been joined, simply set the replication policy, and then issue
+the {command}`coordinators auto` command
+
+For example, assuming we have 3 nodes available, we can enable double
+redundancy mode, then auto-select coordinators. For double redundancy, 3
+coordinators is ideal: therefore FoundationDB will make
+*every* node a coordinator automatically:
+
+```ShellSession
+fdbcli> configure double ssd
+fdbcli> coordinators auto
+```
+
+This will transparently update all the servers within seconds, and
+appropriately rewrite the {command}`fdb.cluster` file, as well as
+informing all client processes to do the same.
+
+## Client connectivity {#module-services-foundationdb-connectivity}
+
+By default, all clients must use the current {command}`fdb.cluster`
+file to access a given FoundationDB cluster. This file is located by default
+in {command}`/etc/foundationdb/fdb.cluster` on all machines with the
+FoundationDB service enabled, so you may copy the active one from your
+cluster to a new node in order to connect, if it is not part of the cluster.
+
+## Client authorization and TLS {#module-services-foundationdb-authorization}
+
+By default, any user who can connect to a FoundationDB process with the
+correct cluster configuration can access anything. FoundationDB uses a
+pluggable design to transport security, and out of the box it supports a
+LibreSSL-based plugin for TLS support. This plugin not only does in-flight
+encryption, but also performs client authorization based on the given
+endpoint's certificate chain. For example, a FoundationDB server may be
+configured to only accept client connections over TLS, where the client TLS
+certificate is from organization *Acme Co* in the
+*Research and Development* unit.
+
+Configuring TLS with FoundationDB is done using the
+{option}`services.foundationdb.tls` options in order to control the
+peer verification string, as well as the certificate and its private key.
+
+Note that the certificate and its private key must be accessible to the
+FoundationDB user account that the server runs under. These files are also
+NOT managed by NixOS, as putting them into the store may reveal private
+information.
+
+After you have a key and certificate file in place, it is not enough to
+simply set the NixOS module options -- you must also configure the
+{command}`fdb.cluster` file to specify that a given set of
+coordinators use TLS. This is as simple as adding the suffix
+{command}`:tls` to your cluster coordinator configuration, after the
+port number. For example, assuming you have a coordinator on localhost with
+the default configuration, simply specifying:
+
+```
+XXXXXX:XXXXXX@127.0.0.1:4500:tls
+```
+
+will configure all clients and server processes to use TLS from now on.
+
+## Backups and Disaster Recovery {#module-services-foundationdb-disaster-recovery}
+
+The usual rules for doing FoundationDB backups apply on NixOS as written in
+the FoundationDB manual. However, one important difference is the security
+profile for NixOS: by default, the {command}`foundationdb` systemd
+unit uses *Linux namespaces* to restrict write access to
+the system, except for the log directory, data directory, and the
+{command}`/etc/foundationdb/` directory. This is enforced by default
+and cannot be disabled.
+
+However, a side effect of this is that the {command}`fdbbackup`
+command doesn't work properly for local filesystem backups: FoundationDB
+uses a server process alongside the database processes to perform backups
+and copy the backups to the filesystem. As a result, this process is put
+under the restricted namespaces above: the backup process can only write to
+a limited number of paths.
+
+In order to allow flexible backup locations on local disks, the FoundationDB
+NixOS module supports a
+{option}`services.foundationdb.extraReadWritePaths` option. This
+option takes a list of paths, and adds them to the systemd unit, allowing
+the processes inside the service to write (and read) the specified
+directories.
+
+For example, to create backups in {command}`/opt/fdb-backups`, first
+set up the paths in the module options:
+
+```
+services.foundationdb.extraReadWritePaths = [ "/opt/fdb-backups" ];
+```
+
+Restart the FoundationDB service, and it will now be able to write to this
+directory (even if it does not yet exist.) Note: this path
+*must* exist before restarting the unit. Otherwise,
+systemd will not include it in the private FoundationDB namespace (and it
+will not add it dynamically at runtime).
+
+You can now perform a backup:
+
+```ShellSession
+$ sudo -u foundationdb fdbbackup start  -t default -d file:///opt/fdb-backups
+$ sudo -u foundationdb fdbbackup status -t default
+```
+
+## Known limitations {#module-services-foundationdb-limitations}
+
+The FoundationDB setup for NixOS should currently be considered beta.
+FoundationDB is not new software, but the NixOS compilation and integration
+has only undergone fairly basic testing of all the available functionality.
+
+  - There is no way to specify individual parameters for individual
+    {command}`fdbserver` processes. Currently, all server processes
+    inherit all the global {command}`fdbmonitor` settings.
+  - Ruby bindings are not currently installed.
+  - Go bindings are not currently installed.
+
+## Options {#module-services-foundationdb-options}
+
+NixOS's FoundationDB module allows you to configure all of the most relevant
+configuration options for {command}`fdbmonitor`, matching it quite
+closely. A complete list of options for the FoundationDB module may be found
+[here](#opt-services.foundationdb.enable). You should
+also read the FoundationDB documentation as well.
+
+## Full documentation {#module-services-foundationdb-full-docs}
+
+FoundationDB is a complex piece of software, and requires careful
+administration to properly use. Full documentation for administration can be
+found here: <https://apple.github.io/foundationdb/>.
diff --git a/nixos/modules/services/databases/foundationdb.nix b/nixos/modules/services/databases/foundationdb.nix
index 16d539b661e..48e9898a68c 100644
--- a/nixos/modules/services/databases/foundationdb.nix
+++ b/nixos/modules/services/databases/foundationdb.nix
@@ -424,6 +424,6 @@ in
     };
   };
 
-  meta.doc         = ./foundationdb.xml;
+  meta.doc         = ./foundationdb.md;
   meta.maintainers = with lib.maintainers; [ thoughtpolice ];
 }
diff --git a/nixos/modules/services/databases/foundationdb.xml b/nixos/modules/services/databases/foundationdb.xml
deleted file mode 100644
index b0b1ebeab45..00000000000
--- a/nixos/modules/services/databases/foundationdb.xml
+++ /dev/null
@@ -1,443 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-foundationdb">
- <title>FoundationDB</title>
- <para>
-  <emphasis>Source:</emphasis>
-  <filename>modules/services/databases/foundationdb.nix</filename>
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="https://apple.github.io/foundationdb/"/>
- </para>
- <para>
-  <emphasis>Maintainer:</emphasis> Austin Seipp
- </para>
- <para>
-  <emphasis>Available version(s):</emphasis> 5.1.x, 5.2.x, 6.0.x
- </para>
- <para>
-  FoundationDB (or "FDB") is an open source, distributed, transactional
-  key-value store.
- </para>
- <section xml:id="module-services-foundationdb-configuring">
-  <title>Configuring and basic setup</title>
-
-  <para>
-   To enable FoundationDB, add the following to your
-   <filename>configuration.nix</filename>:
-<programlisting>
-services.foundationdb.enable = true;
-services.foundationdb.package = pkgs.foundationdb52; # FoundationDB 5.2.x
-</programlisting>
-  </para>
-
-  <para>
-   The <option>services.foundationdb.package</option> option is required, and
-   must always be specified. Due to the fact FoundationDB network protocols and
-   on-disk storage formats may change between (major) versions, and upgrades
-   must be explicitly handled by the user, you must always manually specify
-   this yourself so that the NixOS module will use the proper version. Note
-   that minor, bugfix releases are always compatible.
-  </para>
-
-  <para>
-   After running <command>nixos-rebuild</command>, you can verify whether
-   FoundationDB is running by executing <command>fdbcli</command> (which is
-   added to <option>environment.systemPackages</option>):
-<screen>
-<prompt>$ </prompt>sudo -u foundationdb fdbcli
-Using cluster file `/etc/foundationdb/fdb.cluster'.
-
-The database is available.
-
-Welcome to the fdbcli. For help, type `help'.
-<prompt>fdb> </prompt>status
-
-Using cluster file `/etc/foundationdb/fdb.cluster'.
-
-Configuration:
-  Redundancy mode        - single
-  Storage engine         - memory
-  Coordinators           - 1
-
-Cluster:
-  FoundationDB processes - 1
-  Machines               - 1
-  Memory availability    - 5.4 GB per process on machine with least available
-  Fault Tolerance        - 0 machines
-  Server time            - 04/20/18 15:21:14
-
-...
-
-<prompt>fdb></prompt>
-</screen>
-  </para>
-
-  <para>
-   You can also write programs using the available client libraries. For
-   example, the following Python program can be run in order to grab the
-   cluster status, as a quick example. (This example uses
-   <command>nix-shell</command> shebang support to automatically supply the
-   necessary Python modules).
-<screen>
-<prompt>a@link> </prompt>cat fdb-status.py
-#! /usr/bin/env nix-shell
-#! nix-shell -i python -p python pythonPackages.foundationdb52
-
-import fdb
-import json
-
-def main():
-    fdb.api_version(520)
-    db = fdb.open()
-
-    @fdb.transactional
-    def get_status(tr):
-        return str(tr['\xff\xff/status/json'])
-
-    obj = json.loads(get_status(db))
-    print('FoundationDB available: %s' % obj['client']['database_status']['available'])
-
-if __name__ == "__main__":
-    main()
-<prompt>a@link> </prompt>chmod +x fdb-status.py
-<prompt>a@link> </prompt>./fdb-status.py
-FoundationDB available: True
-<prompt>a@link></prompt>
-</screen>
-  </para>
-
-  <para>
-   FoundationDB is run under the <command>foundationdb</command> user and group
-   by default, but this may be changed in the NixOS configuration. The systemd
-   unit <command>foundationdb.service</command> controls the
-   <command>fdbmonitor</command> process.
-  </para>
-
-  <para>
-   By default, the NixOS module for FoundationDB creates a single SSD-storage
-   based database for development and basic usage. This storage engine is
-   designed for SSDs and will perform poorly on HDDs; however it can handle far
-   more data than the alternative "memory" engine and is a better default
-   choice for most deployments. (Note that you can change the storage backend
-   on-the-fly for a given FoundationDB cluster using
-   <command>fdbcli</command>.)
-  </para>
-
-  <para>
-   Furthermore, only 1 server process and 1 backup agent are started in the
-   default configuration. See below for more on scaling to increase this.
-  </para>
-
-  <para>
-   FoundationDB stores all data for all server processes under
-   <filename>/var/lib/foundationdb</filename>. You can override this using
-   <option>services.foundationdb.dataDir</option>, e.g.
-<programlisting>
-services.foundationdb.dataDir = "/data/fdb";
-</programlisting>
-  </para>
-
-  <para>
-   Similarly, logs are stored under <filename>/var/log/foundationdb</filename>
-   by default, and there is a corresponding
-   <option>services.foundationdb.logDir</option> as well.
-  </para>
- </section>
- <section xml:id="module-services-foundationdb-scaling">
-  <title>Scaling processes and backup agents</title>
-
-  <para>
-   Scaling the number of server processes is quite easy; simply specify
-   <option>services.foundationdb.serverProcesses</option> to be the number of
-   FoundationDB worker processes that should be started on the machine.
-  </para>
-
-  <para>
-   FoundationDB worker processes typically require 4GB of RAM per-process at
-   minimum for good performance, so this option is set to 1 by default since
-   the maximum amount of RAM is unknown. You're advised to abide by this
-   restriction, so pick a number of processes so that each has 4GB or more.
-  </para>
-
-  <para>
-   A similar option exists in order to scale backup agent processes,
-   <option>services.foundationdb.backupProcesses</option>. Backup agents are
-   not as performance/RAM sensitive, so feel free to experiment with the number
-   of available backup processes.
-  </para>
- </section>
- <section xml:id="module-services-foundationdb-clustering">
-  <title>Clustering</title>
-
-  <para>
-   FoundationDB on NixOS works similarly to other Linux systems, so this
-   section will be brief. Please refer to the full FoundationDB documentation
-   for more on clustering.
-  </para>
-
-  <para>
-   FoundationDB organizes clusters using a set of
-   <emphasis>coordinators</emphasis>, which are just specially-designated
-   worker processes. By default, every installation of FoundationDB on NixOS
-   will start as its own individual cluster, with a single coordinator: the
-   first worker process on <command>localhost</command>.
-  </para>
-
-  <para>
-   Coordinators are specified globally using the
-   <command>/etc/foundationdb/fdb.cluster</command> file, which all servers and
-   client applications will use to find and join coordinators. Note that this
-   file <emphasis>can not</emphasis> be managed by NixOS so easily:
-   FoundationDB is designed so that it will rewrite the file at runtime for all
-   clients and nodes when cluster coordinators change, with clients
-   transparently handling this without intervention. It is fundamentally a
-   mutable file, and you should not try to manage it in any way in NixOS.
-  </para>
-
-  <para>
-   When dealing with a cluster, there are two main things you want to do:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Add a node to the cluster for storage/compute.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Promote an ordinary worker to a coordinator.
-    </para>
-   </listitem>
-  </itemizedlist>
-
-  <para>
-   A node must already be a member of the cluster in order to properly be
-   promoted to a coordinator, so you must always add it first if you wish to
-   promote it.
-  </para>
-
-  <para>
-   To add a machine to a FoundationDB cluster:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Choose one of the servers to start as the initial coordinator.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Copy the <command>/etc/foundationdb/fdb.cluster</command> file from this
-     server to all the other servers. Restart FoundationDB on all of these
-     other servers, so they join the cluster.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     All of these servers are now connected and working together in the
-     cluster, under the chosen coordinator.
-    </para>
-   </listitem>
-  </itemizedlist>
-
-  <para>
-   At this point, you can add as many nodes as you want by just repeating the
-   above steps. By default there will still be a single coordinator: you can
-   use <command>fdbcli</command> to change this and add new coordinators.
-  </para>
-
-  <para>
-   As a convenience, FoundationDB can automatically assign coordinators based
-   on the redundancy mode you wish to achieve for the cluster. Once all the
-   nodes have been joined, simply set the replication policy, and then issue
-   the <command>coordinators auto</command> command
-  </para>
-
-  <para>
-   For example, assuming we have 3 nodes available, we can enable double
-   redundancy mode, then auto-select coordinators. For double redundancy, 3
-   coordinators is ideal: therefore FoundationDB will make
-   <emphasis>every</emphasis> node a coordinator automatically:
-  </para>
-
-<screen>
-<prompt>fdbcli> </prompt>configure double ssd
-<prompt>fdbcli> </prompt>coordinators auto
-</screen>
-
-  <para>
-   This will transparently update all the servers within seconds, and
-   appropriately rewrite the <command>fdb.cluster</command> file, as well as
-   informing all client processes to do the same.
-  </para>
- </section>
- <section xml:id="module-services-foundationdb-connectivity">
-  <title>Client connectivity</title>
-
-  <para>
-   By default, all clients must use the current <command>fdb.cluster</command>
-   file to access a given FoundationDB cluster. This file is located by default
-   in <command>/etc/foundationdb/fdb.cluster</command> on all machines with the
-   FoundationDB service enabled, so you may copy the active one from your
-   cluster to a new node in order to connect, if it is not part of the cluster.
-  </para>
- </section>
- <section xml:id="module-services-foundationdb-authorization">
-  <title>Client authorization and TLS</title>
-
-  <para>
-   By default, any user who can connect to a FoundationDB process with the
-   correct cluster configuration can access anything. FoundationDB uses a
-   pluggable design to transport security, and out of the box it supports a
-   LibreSSL-based plugin for TLS support. This plugin not only does in-flight
-   encryption, but also performs client authorization based on the given
-   endpoint's certificate chain. For example, a FoundationDB server may be
-   configured to only accept client connections over TLS, where the client TLS
-   certificate is from organization <emphasis>Acme Co</emphasis> in the
-   <emphasis>Research and Development</emphasis> unit.
-  </para>
-
-  <para>
-   Configuring TLS with FoundationDB is done using the
-   <option>services.foundationdb.tls</option> options in order to control the
-   peer verification string, as well as the certificate and its private key.
-  </para>
-
-  <para>
-   Note that the certificate and its private key must be accessible to the
-   FoundationDB user account that the server runs under. These files are also
-   NOT managed by NixOS, as putting them into the store may reveal private
-   information.
-  </para>
-
-  <para>
-   After you have a key and certificate file in place, it is not enough to
-   simply set the NixOS module options -- you must also configure the
-   <command>fdb.cluster</command> file to specify that a given set of
-   coordinators use TLS. This is as simple as adding the suffix
-   <command>:tls</command> to your cluster coordinator configuration, after the
-   port number. For example, assuming you have a coordinator on localhost with
-   the default configuration, simply specifying:
-  </para>
-
-<programlisting>
-XXXXXX:XXXXXX@127.0.0.1:4500:tls
-</programlisting>
-
-  <para>
-   will configure all clients and server processes to use TLS from now on.
-  </para>
- </section>
- <section xml:id="module-services-foundationdb-disaster-recovery">
-  <title>Backups and Disaster Recovery</title>
-
-  <para>
-   The usual rules for doing FoundationDB backups apply on NixOS as written in
-   the FoundationDB manual. However, one important difference is the security
-   profile for NixOS: by default, the <command>foundationdb</command> systemd
-   unit uses <emphasis>Linux namespaces</emphasis> to restrict write access to
-   the system, except for the log directory, data directory, and the
-   <command>/etc/foundationdb/</command> directory. This is enforced by default
-   and cannot be disabled.
-  </para>
-
-  <para>
-   However, a side effect of this is that the <command>fdbbackup</command>
-   command doesn't work properly for local filesystem backups: FoundationDB
-   uses a server process alongside the database processes to perform backups
-   and copy the backups to the filesystem. As a result, this process is put
-   under the restricted namespaces above: the backup process can only write to
-   a limited number of paths.
-  </para>
-
-  <para>
-   In order to allow flexible backup locations on local disks, the FoundationDB
-   NixOS module supports a
-   <option>services.foundationdb.extraReadWritePaths</option> option. This
-   option takes a list of paths, and adds them to the systemd unit, allowing
-   the processes inside the service to write (and read) the specified
-   directories.
-  </para>
-
-  <para>
-   For example, to create backups in <command>/opt/fdb-backups</command>, first
-   set up the paths in the module options:
-  </para>
-
-<programlisting>
-services.foundationdb.extraReadWritePaths = [ "/opt/fdb-backups" ];
-</programlisting>
-
-  <para>
-   Restart the FoundationDB service, and it will now be able to write to this
-   directory (even if it does not yet exist.) Note: this path
-   <emphasis>must</emphasis> exist before restarting the unit. Otherwise,
-   systemd will not include it in the private FoundationDB namespace (and it
-   will not add it dynamically at runtime).
-  </para>
-
-  <para>
-   You can now perform a backup:
-  </para>
-
-<screen>
-<prompt>$ </prompt>sudo -u foundationdb fdbbackup start  -t default -d file:///opt/fdb-backups
-<prompt>$ </prompt>sudo -u foundationdb fdbbackup status -t default
-</screen>
- </section>
- <section xml:id="module-services-foundationdb-limitations">
-  <title>Known limitations</title>
-
-  <para>
-   The FoundationDB setup for NixOS should currently be considered beta.
-   FoundationDB is not new software, but the NixOS compilation and integration
-   has only undergone fairly basic testing of all the available functionality.
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     There is no way to specify individual parameters for individual
-     <command>fdbserver</command> processes. Currently, all server processes
-     inherit all the global <command>fdbmonitor</command> settings.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Ruby bindings are not currently installed.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Go bindings are not currently installed.
-    </para>
-   </listitem>
-  </itemizedlist>
- </section>
- <section xml:id="module-services-foundationdb-options">
-  <title>Options</title>
-
-  <para>
-   NixOS's FoundationDB module allows you to configure all of the most relevant
-   configuration options for <command>fdbmonitor</command>, matching it quite
-   closely. A complete list of options for the FoundationDB module may be found
-   <link linkend="opt-services.foundationdb.enable">here</link>. You should
-   also read the FoundationDB documentation as well.
-  </para>
- </section>
- <section xml:id="module-services-foundationdb-full-docs">
-  <title>Full documentation</title>
-
-  <para>
-   FoundationDB is a complex piece of software, and requires careful
-   administration to properly use. Full documentation for administration can be
-   found here: <link xlink:href="https://apple.github.io/foundationdb/"/>.
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/databases/lldap.nix b/nixos/modules/services/databases/lldap.nix
new file mode 100644
index 00000000000..960792d0805
--- /dev/null
+++ b/nixos/modules/services/databases/lldap.nix
@@ -0,0 +1,121 @@
+{ config, lib, pkgs, utils, ... }:
+
+let
+  cfg = config.services.lldap;
+  format = pkgs.formats.toml { };
+in
+{
+  options.services.lldap = with lib; {
+    enable = mkEnableOption (mdDoc "lldap");
+
+    package = mkPackageOptionMD pkgs "lldap" { };
+
+    environment = mkOption {
+      type = with types; attrsOf str;
+      default = { };
+      example = {
+        LLDAP_JWT_SECRET_FILE = "/run/lldap/jwt_secret";
+        LLDAP_LDAP_USER_PASS_FILE = "/run/lldap/user_password";
+      };
+      description = lib.mdDoc ''
+        Environment variables passed to the service.
+        Any config option name prefixed with `LLDAP_` takes priority over the one in the configuration file.
+      '';
+    };
+
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = lib.mdDoc ''
+        Environment file as defined in {manpage}`systemd.exec(5)` passed to the service.
+      '';
+    };
+
+    settings = mkOption {
+      description = mdDoc ''
+        Free-form settings written directly to the `lldap_config.toml` file.
+        Refer to <https://github.com/lldap/lldap/blob/main/lldap_config.docker_template.toml> for supported values.
+      '';
+
+      default = { };
+
+      type = types.submodule {
+        freeformType = format.type;
+        options = {
+          ldap_host = mkOption {
+            type = types.str;
+            description = mdDoc "The host address that the LDAP server will be bound to.";
+            default = "::";
+          };
+
+          ldap_port = mkOption {
+            type = types.port;
+            description = mdDoc "The port on which to have the LDAP server.";
+            default = 3890;
+          };
+
+          http_host = mkOption {
+            type = types.str;
+            description = mdDoc "The host address that the HTTP server will be bound to.";
+            default = "::";
+          };
+
+          http_port = mkOption {
+            type = types.port;
+            description = mdDoc "The port on which to have the HTTP server, for user login and administration.";
+            default = 17170;
+          };
+
+          http_url = mkOption {
+            type = types.str;
+            description = mdDoc "The public URL of the server, for password reset links.";
+            default = "http://localhost";
+          };
+
+          ldap_base_dn = mkOption {
+            type = types.str;
+            description = mdDoc "Base DN for LDAP.";
+            example = "dc=example,dc=com";
+          };
+
+          ldap_user_dn = mkOption {
+            type = types.str;
+            description = mdDoc "Admin username";
+            default = "admin";
+          };
+
+          ldap_user_email = mkOption {
+            type = types.str;
+            description = mdDoc "Admin email.";
+            default = "admin@example.com";
+          };
+
+          database_url = mkOption {
+            type = types.str;
+            description = mdDoc "Database URL.";
+            default = "sqlite://./users.db?mode=rwc";
+            example = "postgres://postgres-user:password@postgres-server/my-database";
+          };
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.lldap = {
+      description = "Lightweight LDAP server (lldap)";
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${lib.getExe cfg.package} run --config-file ${format.generate "lldap_config.toml" cfg.settings}";
+        StateDirectory = "lldap";
+        WorkingDirectory = "%S/lldap";
+        User = "lldap";
+        Group = "lldap";
+        DynamicUser = true;
+        EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
+      };
+      inherit (cfg) environment;
+    };
+  };
+}
diff --git a/nixos/modules/services/databases/mongodb.nix b/nixos/modules/services/databases/mongodb.nix
index 211133de63f..8f3be1492e9 100644
--- a/nixos/modules/services/databases/mongodb.nix
+++ b/nixos/modules/services/databases/mongodb.nix
@@ -142,7 +142,7 @@ in
           User = cfg.user;
           PIDFile = cfg.pidFile;
           Type = "forking";
-          TimeoutStartSec=120; # intial creating of journal can take some time
+          TimeoutStartSec=120; # initial creating of journal can take some time
           PermissionsStartOnly = true;
         };
 
diff --git a/nixos/modules/services/databases/neo4j.nix b/nixos/modules/services/databases/neo4j.nix
index d78ff8390e4..09050242402 100644
--- a/nixos/modules/services/databases/neo4j.nix
+++ b/nixos/modules/services/databases/neo4j.nix
@@ -636,6 +636,6 @@ in {
     };
 
   meta = {
-    maintainers = with lib.maintainers; [ patternspandemic jonringer erictapen ];
+    maintainers = with lib.maintainers; [ patternspandemic jonringer ];
   };
 }
diff --git a/nixos/modules/services/databases/postgresql.md b/nixos/modules/services/databases/postgresql.md
new file mode 100644
index 00000000000..4d66ee38be4
--- /dev/null
+++ b/nixos/modules/services/databases/postgresql.md
@@ -0,0 +1,210 @@
+# PostgreSQL {#module-postgresql}
+
+<!-- FIXME: render nicely -->
+<!-- FIXME: source can be added automatically -->
+
+*Source:* {file}`modules/services/databases/postgresql.nix`
+
+*Upstream documentation:* <http://www.postgresql.org/docs/>
+
+<!-- FIXME: more stuff, like maintainer? -->
+
+PostgreSQL is an advanced, free relational database.
+<!-- MORE -->
+
+## Configuring {#module-services-postgres-configuring}
+
+To enable PostgreSQL, add the following to your {file}`configuration.nix`:
+```
+services.postgresql.enable = true;
+services.postgresql.package = pkgs.postgresql_11;
+```
+Note that you are required to specify the desired version of PostgreSQL (e.g. `pkgs.postgresql_11`). Since upgrading your PostgreSQL version requires a database dump and reload (see below), NixOS cannot provide a default value for [](#opt-services.postgresql.package) such as the most recent release of PostgreSQL.
+
+<!--
+After running {command}`nixos-rebuild`, you can verify
+whether PostgreSQL works by running {command}`psql`:
+
+```ShellSession
+$ psql
+psql (9.2.9)
+Type "help" for help.
+
+alice=>
+```
+-->
+
+By default, PostgreSQL stores its databases in {file}`/var/lib/postgresql/$psqlSchema`. You can override this using [](#opt-services.postgresql.dataDir), e.g.
+```
+services.postgresql.dataDir = "/data/postgresql";
+```
+
+## Upgrading {#module-services-postgres-upgrading}
+
+::: {.note}
+The steps below demonstrate how to upgrade from an older version to `pkgs.postgresql_13`.
+These instructions are also applicable to other versions.
+:::
+
+Major PostgreSQL upgrades require a downtime and a few imperative steps to be called. This is the case because
+each major version has some internal changes in the databases' state during major releases. Because of that,
+NixOS places the state into {file}`/var/lib/postgresql/&lt;version&gt;` where each `version`
+can be obtained like this:
+```
+$ nix-instantiate --eval -A postgresql_13.psqlSchema
+"13"
+```
+For an upgrade, a script like this can be used to simplify the process:
+```
+{ config, pkgs, ... }:
+{
+  environment.systemPackages = [
+    (let
+      # XXX specify the postgresql package you'd like to upgrade to.
+      # Do not forget to list the extensions you need.
+      newPostgres = pkgs.postgresql_13.withPackages (pp: [
+        # pp.plv8
+      ]);
+    in pkgs.writeScriptBin "upgrade-pg-cluster" ''
+      set -eux
+      # XXX it's perhaps advisable to stop all services that depend on postgresql
+      systemctl stop postgresql
+
+      export NEWDATA="/var/lib/postgresql/${newPostgres.psqlSchema}"
+
+      export NEWBIN="${newPostgres}/bin"
+
+      export OLDDATA="${config.services.postgresql.dataDir}"
+      export OLDBIN="${config.services.postgresql.package}/bin"
+
+      install -d -m 0700 -o postgres -g postgres "$NEWDATA"
+      cd "$NEWDATA"
+      sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
+
+      sudo -u postgres $NEWBIN/pg_upgrade \
+        --old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
+        --old-bindir $OLDBIN --new-bindir $NEWBIN \
+        "$@"
+    '')
+  ];
+}
+```
+
+The upgrade process is:
+
+  1. Rebuild nixos configuration with the configuration above added to your {file}`configuration.nix`. Alternatively, add that into separate file and reference it in `imports` list.
+  2. Login as root (`sudo su -`)
+  3. Run `upgrade-pg-cluster`. It will stop old postgresql, initialize a new one and migrate the old one to the new one. You may supply arguments like `--jobs 4` and `--link` to speedup migration process. See <https://www.postgresql.org/docs/current/pgupgrade.html> for details.
+  4. Change postgresql package in NixOS configuration to the one you were upgrading to via [](#opt-services.postgresql.package). Rebuild NixOS. This should start new postgres using upgraded data directory and all services you stopped during the upgrade.
+  5. After the upgrade it's advisable to analyze the new cluster.
+
+       - For PostgreSQL ≥ 14, use the `vacuumdb` command printed by the upgrades script.
+       - For PostgreSQL < 14, run (as `su -l postgres` in the [](#opt-services.postgresql.dataDir), in this example {file}`/var/lib/postgresql/13`):
+
+         ```
+         $ ./analyze_new_cluster.sh
+         ```
+
+     ::: {.warning}
+     The next step removes the old state-directory!
+     :::
+
+     ```
+     $ ./delete_old_cluster.sh
+     ```
+
+## Options {#module-services-postgres-options}
+
+A complete list of options for the PostgreSQL module may be found [here](#opt-services.postgresql.enable).
+
+## Plugins {#module-services-postgres-plugins}
+
+Plugins collection for each PostgreSQL version can be accessed with `.pkgs`. For example, for `pkgs.postgresql_11` package, its plugin collection is accessed by `pkgs.postgresql_11.pkgs`:
+```ShellSession
+$ nix repl '<nixpkgs>'
+
+Loading '<nixpkgs>'...
+Added 10574 variables.
+
+nix-repl> postgresql_11.pkgs.<TAB><TAB>
+postgresql_11.pkgs.cstore_fdw        postgresql_11.pkgs.pg_repack
+postgresql_11.pkgs.pg_auto_failover  postgresql_11.pkgs.pg_safeupdate
+postgresql_11.pkgs.pg_bigm           postgresql_11.pkgs.pg_similarity
+postgresql_11.pkgs.pg_cron           postgresql_11.pkgs.pg_topn
+postgresql_11.pkgs.pg_hll            postgresql_11.pkgs.pgjwt
+postgresql_11.pkgs.pg_partman        postgresql_11.pkgs.pgroonga
+...
+```
+
+To add plugins via NixOS configuration, set `services.postgresql.extraPlugins`:
+```
+services.postgresql.package = pkgs.postgresql_11;
+services.postgresql.extraPlugins = with pkgs.postgresql_11.pkgs; [
+  pg_repack
+  postgis
+];
+```
+
+You can build custom PostgreSQL-with-plugins (to be used outside of NixOS) using function `.withPackages`. For example, creating a custom PostgreSQL package in an overlay can look like:
+```
+self: super: {
+  postgresql_custom = self.postgresql_11.withPackages (ps: [
+    ps.pg_repack
+    ps.postgis
+  ]);
+}
+```
+
+Here's a recipe on how to override a particular plugin through an overlay:
+```
+self: super: {
+  postgresql_11 = super.postgresql_11.override { this = self.postgresql_11; } // {
+    pkgs = super.postgresql_11.pkgs // {
+      pg_repack = super.postgresql_11.pkgs.pg_repack.overrideAttrs (_: {
+        name = "pg_repack-v20181024";
+        src = self.fetchzip {
+          url = "https://github.com/reorg/pg_repack/archive/923fa2f3c709a506e111cc963034bf2fd127aa00.tar.gz";
+          sha256 = "17k6hq9xaax87yz79j773qyigm4fwk8z4zh5cyp6z0sxnwfqxxw5";
+        };
+      });
+    };
+  };
+}
+```
+
+## JIT (Just-In-Time compilation) {#module-services-postgres-jit}
+
+[JIT](https://www.postgresql.org/docs/current/jit-reason.html)-support in the PostgreSQL package
+is disabled by default because of the ~300MiB closure-size increase from the LLVM dependency. It
+can be optionally enabled in PostgreSQL with the following config option:
+
+```nix
+{
+  services.postgresql.enableJIT = true;
+}
+```
+
+This makes sure that the [`jit`](https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-JIT)-setting
+is set to `on` and a PostgreSQL package with JIT enabled is used. Further tweaking of the JIT compiler, e.g. setting a different
+query cost threshold via [`jit_above_cost`](https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-JIT-ABOVE-COST)
+can be done manually via [`services.postgresql.settings`](#opt-services.postgresql.settings).
+
+The attribute-names of JIT-enabled PostgreSQL packages are suffixed with `_jit`, i.e. for each `pkgs.postgresql`
+(and `pkgs.postgresql_<major>`) in `nixpkgs` there's also a `pkgs.postgresql_jit` (and `pkgs.postgresql_<major>_jit`).
+Alternatively, a JIT-enabled variant can be derived from a given `postgresql` package via `postgresql.withJIT`.
+This is also useful if it's not clear which attribute from `nixpkgs` was originally used (e.g. when working with
+[`config.services.postgresql.package`](#opt-services.postgresql.package) or if the package was modified via an
+overlay) since all modifications are propagated to `withJIT`. I.e.
+
+```nix
+with import <nixpkgs> {
+  overlays = [
+    (self: super: {
+      postgresql = super.postgresql.overrideAttrs (_: { pname = "foobar"; });
+    })
+  ];
+};
+postgresql.withJIT.pname
+```
+
+evaluates to `"foobar"`.
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
index 6665e7a088f..a7016bbee3a 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -7,9 +7,18 @@ let
   cfg = config.services.postgresql;
 
   postgresql =
+    let
+      # ensure that
+      #   services.postgresql = {
+      #     enableJIT = true;
+      #     package = pkgs.postgresql_<major>;
+      #   };
+      # works.
+      base = if cfg.enableJIT && !cfg.package.jitSupport then cfg.package.withJIT else cfg.package;
+    in
     if cfg.extraPlugins == []
-      then cfg.package
-      else cfg.package.withPackages (_: cfg.extraPlugins);
+      then base
+      else base.withPackages (_: cfg.extraPlugins);
 
   toStr = value:
     if true == value then "yes"
@@ -42,6 +51,8 @@ in
 
       enable = mkEnableOption (lib.mdDoc "PostgreSQL Server");
 
+      enableJIT = mkEnableOption (lib.mdDoc "JIT support");
+
       package = mkOption {
         type = types.package;
         example = literalExpression "pkgs.postgresql_11";
@@ -435,19 +446,21 @@ in
         log_line_prefix = cfg.logLinePrefix;
         listen_addresses = if cfg.enableTCPIP then "*" else "localhost";
         port = cfg.port;
+        jit = mkDefault (if cfg.enableJIT then "on" else "off");
       };
 
     services.postgresql.package = let
         mkThrow = ver: throw "postgresql_${ver} was removed, please upgrade your postgresql version.";
+        base = if versionAtLeast config.system.stateVersion "22.05" then pkgs.postgresql_14
+            else if versionAtLeast config.system.stateVersion "21.11" then pkgs.postgresql_13
+            else if versionAtLeast config.system.stateVersion "20.03" then pkgs.postgresql_11
+            else if versionAtLeast config.system.stateVersion "17.09" then mkThrow "9_6"
+            else mkThrow "9_5";
     in
       # Note: when changing the default, make it conditional on
       # ‘system.stateVersion’ to maintain compatibility with existing
       # systems!
-      mkDefault (if versionAtLeast config.system.stateVersion "22.05" then pkgs.postgresql_14
-            else if versionAtLeast config.system.stateVersion "21.11" then pkgs.postgresql_13
-            else if versionAtLeast config.system.stateVersion "20.03" then pkgs.postgresql_11
-            else if versionAtLeast config.system.stateVersion "17.09" then mkThrow "9_6"
-            else mkThrow "9_5");
+      mkDefault (if cfg.enableJIT then base.withJIT else base);
 
     services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
 
@@ -476,7 +489,7 @@ in
      "/share/postgresql"
     ];
 
-    system.extraDependencies = lib.optional (cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) configFileCheck;
+    system.checks = lib.optional (cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) configFileCheck;
 
     systemd.services.postgresql =
       { description = "PostgreSQL Server";
@@ -585,6 +598,6 @@ in
 
   };
 
-  meta.doc = ./postgresql.xml;
+  meta.doc = ./postgresql.md;
   meta.maintainers = with lib.maintainers; [ thoughtpolice danbst ];
 }
diff --git a/nixos/modules/services/databases/postgresql.xml b/nixos/modules/services/databases/postgresql.xml
deleted file mode 100644
index e48c578e6ce..00000000000
--- a/nixos/modules/services/databases/postgresql.xml
+++ /dev/null
@@ -1,231 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-postgresql">
- <title>PostgreSQL</title>
-<!-- FIXME: render nicely -->
-<!-- FIXME: source can be added automatically -->
- <para>
-  <emphasis>Source:</emphasis> <filename>modules/services/databases/postgresql.nix</filename>
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis> <link xlink:href="http://www.postgresql.org/docs/"/>
- </para>
-<!-- FIXME: more stuff, like maintainer? -->
- <para>
-  PostgreSQL is an advanced, free relational database.
-<!-- MORE -->
- </para>
- <section xml:id="module-services-postgres-configuring">
-  <title>Configuring</title>
-
-  <para>
-   To enable PostgreSQL, add the following to your <filename>configuration.nix</filename>:
-<programlisting>
-<xref linkend="opt-services.postgresql.enable"/> = true;
-<xref linkend="opt-services.postgresql.package"/> = pkgs.postgresql_11;
-</programlisting>
-   Note that you are required to specify the desired version of PostgreSQL (e.g. <literal>pkgs.postgresql_11</literal>). Since upgrading your PostgreSQL version requires a database dump and reload (see below), NixOS cannot provide a default value for <xref linkend="opt-services.postgresql.package"/> such as the most recent release of PostgreSQL.
-  </para>
-
-<!--
-<para>After running <command>nixos-rebuild</command>, you can verify
-whether PostgreSQL works by running <command>psql</command>:
-
-<screen>
-<prompt>$ </prompt>psql
-psql (9.2.9)
-Type "help" for help.
-
-<prompt>alice=></prompt>
-</screen>
--->
-
-  <para>
-   By default, PostgreSQL stores its databases in <filename>/var/lib/postgresql/$psqlSchema</filename>. You can override this using <xref linkend="opt-services.postgresql.dataDir"/>, e.g.
-<programlisting>
-<xref linkend="opt-services.postgresql.dataDir"/> = "/data/postgresql";
-</programlisting>
-  </para>
- </section>
- <section xml:id="module-services-postgres-upgrading">
-  <title>Upgrading</title>
-
-  <note>
-   <para>
-    The steps below demonstrate how to upgrade from an older version to <package>pkgs.postgresql_13</package>.
-    These instructions are also applicable to other versions.
-   </para>
-  </note>
-  <para>
-   Major PostgreSQL upgrades require a downtime and a few imperative steps to be called. This is the case because
-   each major version has some internal changes in the databases' state during major releases. Because of that,
-   NixOS places the state into <filename>/var/lib/postgresql/&lt;version&gt;</filename> where each <literal>version</literal>
-   can be obtained like this:
-<programlisting>
-<prompt>$ </prompt>nix-instantiate --eval -A postgresql_13.psqlSchema
-"13"
-</programlisting>
-   For an upgrade, a script like this can be used to simplify the process:
-<programlisting>
-{ config, pkgs, ... }:
-{
-  <xref linkend="opt-environment.systemPackages" /> = [
-    (let
-      # XXX specify the postgresql package you'd like to upgrade to.
-      # Do not forget to list the extensions you need.
-      newPostgres = pkgs.postgresql_13.withPackages (pp: [
-        # pp.plv8
-      ]);
-    in pkgs.writeScriptBin "upgrade-pg-cluster" ''
-      set -eux
-      # XXX it's perhaps advisable to stop all services that depend on postgresql
-      systemctl stop postgresql
-
-      export NEWDATA="/var/lib/postgresql/${newPostgres.psqlSchema}"
-
-      export NEWBIN="${newPostgres}/bin"
-
-      export OLDDATA="${config.<xref linkend="opt-services.postgresql.dataDir"/>}"
-      export OLDBIN="${config.<xref linkend="opt-services.postgresql.package"/>}/bin"
-
-      install -d -m 0700 -o postgres -g postgres "$NEWDATA"
-      cd "$NEWDATA"
-      sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
-
-      sudo -u postgres $NEWBIN/pg_upgrade \
-        --old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
-        --old-bindir $OLDBIN --new-bindir $NEWBIN \
-        "$@"
-    '')
-  ];
-}
-</programlisting>
-  </para>
-
-  <para>
-   The upgrade process is:
-  </para>
-
-  <orderedlist>
-   <listitem>
-    <para>
-     Rebuild nixos configuration with the configuration above added to your <filename>configuration.nix</filename>. Alternatively, add that into separate file and reference it in <literal>imports</literal> list.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Login as root (<literal>sudo su -</literal>)
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Run <literal>upgrade-pg-cluster</literal>. It will stop old postgresql, initialize a new one and migrate the old one to the new one. You may supply arguments like <literal>--jobs 4</literal> and <literal>--link</literal> to speedup migration process. See <link xlink:href="https://www.postgresql.org/docs/current/pgupgrade.html" /> for details.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Change postgresql package in NixOS configuration to the one you were upgrading to via <xref linkend="opt-services.postgresql.package" />. Rebuild NixOS. This should start new postgres using upgraded data directory and all services you stopped during the upgrade.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     After the upgrade it's advisable to analyze the new cluster.
-    </para>
-    <itemizedlist>
-     <listitem>
-      <para>
-       For PostgreSQL ≥ 14, use the <literal>vacuumdb</literal> command printed by the upgrades script.
-      </para>
-     </listitem>
-     <listitem>
-       <para>
-        For PostgreSQL &lt; 14, run (as <literal>su -l postgres</literal> in the <xref linkend="opt-services.postgresql.dataDir" />, in this example <filename>/var/lib/postgresql/13</filename>):
-<programlisting>
-<prompt>$ </prompt>./analyze_new_cluster.sh
-</programlisting>
-       </para>
-     </listitem>
-    </itemizedlist>
-    <para>
-      <warning><para>The next step removes the old state-directory!</para></warning>
-<programlisting>
-<prompt>$ </prompt>./delete_old_cluster.sh
-</programlisting>
-    </para>
-   </listitem>
-  </orderedlist>
- </section>
- <section xml:id="module-services-postgres-options">
-  <title>Options</title>
-
-  <para>
-   A complete list of options for the PostgreSQL module may be found <link linkend="opt-services.postgresql.enable">here</link>.
-  </para>
- </section>
- <section xml:id="module-services-postgres-plugins">
-  <title>Plugins</title>
-
-  <para>
-   Plugins collection for each PostgreSQL version can be accessed with <literal>.pkgs</literal>. For example, for <literal>pkgs.postgresql_11</literal> package, its plugin collection is accessed by <literal>pkgs.postgresql_11.pkgs</literal>:
-<screen>
-<prompt>$ </prompt>nix repl '&lt;nixpkgs&gt;'
-
-Loading '&lt;nixpkgs&gt;'...
-Added 10574 variables.
-
-<prompt>nix-repl&gt; </prompt>postgresql_11.pkgs.&lt;TAB&gt;&lt;TAB&gt;
-postgresql_11.pkgs.cstore_fdw        postgresql_11.pkgs.pg_repack
-postgresql_11.pkgs.pg_auto_failover  postgresql_11.pkgs.pg_safeupdate
-postgresql_11.pkgs.pg_bigm           postgresql_11.pkgs.pg_similarity
-postgresql_11.pkgs.pg_cron           postgresql_11.pkgs.pg_topn
-postgresql_11.pkgs.pg_hll            postgresql_11.pkgs.pgjwt
-postgresql_11.pkgs.pg_partman        postgresql_11.pkgs.pgroonga
-...
-</screen>
-  </para>
-
-  <para>
-   To add plugins via NixOS configuration, set <literal>services.postgresql.extraPlugins</literal>:
-<programlisting>
-<xref linkend="opt-services.postgresql.package"/> = pkgs.postgresql_11;
-<xref linkend="opt-services.postgresql.extraPlugins"/> = with pkgs.postgresql_11.pkgs; [
-  pg_repack
-  postgis
-];
-</programlisting>
-  </para>
-
-  <para>
-   You can build custom PostgreSQL-with-plugins (to be used outside of NixOS) using function <literal>.withPackages</literal>. For example, creating a custom PostgreSQL package in an overlay can look like:
-<programlisting>
-self: super: {
-  postgresql_custom = self.postgresql_11.withPackages (ps: [
-    ps.pg_repack
-    ps.postgis
-  ]);
-}
-</programlisting>
-  </para>
-
-  <para>
-   Here's a recipe on how to override a particular plugin through an overlay:
-<programlisting>
-self: super: {
-  postgresql_11 = super.postgresql_11.override { this = self.postgresql_11; } // {
-    pkgs = super.postgresql_11.pkgs // {
-      pg_repack = super.postgresql_11.pkgs.pg_repack.overrideAttrs (_: {
-        name = "pg_repack-v20181024";
-        src = self.fetchzip {
-          url = "https://github.com/reorg/pg_repack/archive/923fa2f3c709a506e111cc963034bf2fd127aa00.tar.gz";
-          sha256 = "17k6hq9xaax87yz79j773qyigm4fwk8z4zh5cyp6z0sxnwfqxxw5";
-        };
-      });
-    };
-  };
-}
-</programlisting>
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/desktops/deepin/app-services.nix b/nixos/modules/services/desktops/deepin/app-services.nix
new file mode 100644
index 00000000000..6f9932e4873
--- /dev/null
+++ b/nixos/modules/services/desktops/deepin/app-services.nix
@@ -0,0 +1,36 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.deepin.members;
+  };
+
+  ###### interface
+
+  options = {
+
+    services.deepin.app-services = {
+
+      enable = mkEnableOption (lib.mdDoc "Service collection of DDE applications, including dconfig-center");
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.deepin.app-services.enable {
+
+    environment.systemPackages = [ pkgs.deepin.dde-app-services ];
+
+    services.dbus.packages = [ pkgs.deepin.dde-app-services ];
+
+    environment.pathsToLink = [ "/share/dsg" ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/deepin/dde-api.nix b/nixos/modules/services/desktops/deepin/dde-api.nix
new file mode 100644
index 00000000000..472d9860c10
--- /dev/null
+++ b/nixos/modules/services/desktops/deepin/dde-api.nix
@@ -0,0 +1,50 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.deepin.members;
+  };
+
+  ###### interface
+
+  options = {
+
+    services.deepin.dde-api = {
+
+      enable = mkEnableOption (lib.mdDoc ''
+        Provides some dbus interfaces that is used for screen zone detecting,
+        thumbnail generating, and sound playing in Deepin Desktop Environment.
+      '');
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.deepin.dde-api.enable {
+
+     environment.systemPackages = [ pkgs.deepin.dde-api ];
+
+     services.dbus.packages = [ pkgs.deepin.dde-api ];
+
+     systemd.packages = [ pkgs.deepin.dde-api ];
+
+     environment.pathsToLink = [ "/lib/deepin-api" ];
+
+     users.groups.deepin-sound-player = { };
+     users.users.deepin-sound-player = {
+       description = "Deepin sound player";
+       home = "/var/lib/deepin-sound-player";
+       createHome = true;
+       group = "deepin-sound-player";
+       isSystemUser = true;
+     };
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/deepin/dde-daemon.nix b/nixos/modules/services/desktops/deepin/dde-daemon.nix
new file mode 100644
index 00000000000..9377f523ebf
--- /dev/null
+++ b/nixos/modules/services/desktops/deepin/dde-daemon.nix
@@ -0,0 +1,40 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.deepin.members;
+  };
+
+  ###### interface
+
+  options = {
+
+    services.deepin.dde-daemon = {
+
+      enable = mkEnableOption (lib.mdDoc "Daemon for handling the deepin session settings");
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.deepin.dde-daemon.enable {
+
+    environment.systemPackages = [ pkgs.deepin.dde-daemon ];
+
+    services.dbus.packages = [ pkgs.deepin.dde-daemon ];
+
+    services.udev.packages = [ pkgs.deepin.dde-daemon ];
+
+    systemd.packages = [ pkgs.deepin.dde-daemon ];
+
+    environment.pathsToLink = [ "/lib/deepin-daemon" ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/flatpak.md b/nixos/modules/services/desktops/flatpak.md
new file mode 100644
index 00000000000..65b1554d79b
--- /dev/null
+++ b/nixos/modules/services/desktops/flatpak.md
@@ -0,0 +1,39 @@
+# Flatpak {#module-services-flatpak}
+
+*Source:* {file}`modules/services/desktop/flatpak.nix`
+
+*Upstream documentation:* <https://github.com/flatpak/flatpak/wiki>
+
+Flatpak is a system for building, distributing, and running sandboxed desktop
+applications on Linux.
+
+To enable Flatpak, add the following to your {file}`configuration.nix`:
+```
+  services.flatpak.enable = true;
+```
+
+For the sandboxed apps to work correctly, desktop integration portals need to
+be installed. If you run GNOME, this will be handled automatically for you;
+in other cases, you will need to add something like the following to your
+{file}`configuration.nix`:
+```
+  xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
+```
+
+Then, you will need to add a repository, for example,
+[Flathub](https://github.com/flatpak/flatpak/wiki),
+either using the following commands:
+```ShellSession
+$ flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
+$ flatpak update
+```
+or by opening the
+[repository file](https://flathub.org/repo/flathub.flatpakrepo) in GNOME Software.
+
+Finally, you can search and install programs:
+```ShellSession
+$ flatpak search bustle
+$ flatpak install flathub org.freedesktop.Bustle
+$ flatpak run org.freedesktop.Bustle
+```
+Again, GNOME Software offers graphical interface for these tasks.
diff --git a/nixos/modules/services/desktops/flatpak.nix b/nixos/modules/services/desktops/flatpak.nix
index 3b14ad75ab3..d99faf381e0 100644
--- a/nixos/modules/services/desktops/flatpak.nix
+++ b/nixos/modules/services/desktops/flatpak.nix
@@ -7,7 +7,7 @@ let
   cfg = config.services.flatpak;
 in {
   meta = {
-    doc = ./flatpak.xml;
+    doc = ./flatpak.md;
     maintainers = pkgs.flatpak.meta.maintainers;
   };
 
diff --git a/nixos/modules/services/desktops/flatpak.xml b/nixos/modules/services/desktops/flatpak.xml
deleted file mode 100644
index 8f080b25022..00000000000
--- a/nixos/modules/services/desktops/flatpak.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-flatpak">
- <title>Flatpak</title>
- <para>
-  <emphasis>Source:</emphasis>
-  <filename>modules/services/desktop/flatpak.nix</filename>
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="https://github.com/flatpak/flatpak/wiki"/>
- </para>
- <para>
-  Flatpak is a system for building, distributing, and running sandboxed desktop
-  applications on Linux.
- </para>
- <para>
-  To enable Flatpak, add the following to your
-  <filename>configuration.nix</filename>:
-<programlisting>
-  <xref linkend="opt-services.flatpak.enable"/> = true;
-</programlisting>
- </para>
- <para>
-  For the sandboxed apps to work correctly, desktop integration portals need to
-  be installed. If you run GNOME, this will be handled automatically for you;
-  in other cases, you will need to add something like the following to your
-  <filename>configuration.nix</filename>:
-<programlisting>
-  <xref linkend="opt-xdg.portal.extraPortals"/> = [ pkgs.xdg-desktop-portal-gtk ];
-</programlisting>
- </para>
- <para>
-  Then, you will need to add a repository, for example,
-  <link xlink:href="https://github.com/flatpak/flatpak/wiki">Flathub</link>,
-  either using the following commands:
-<screen>
-<prompt>$ </prompt>flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
-<prompt>$ </prompt>flatpak update
-</screen>
-  or by opening the
-  <link xlink:href="https://flathub.org/repo/flathub.flatpakrepo">repository
-  file</link> in GNOME Software.
- </para>
- <para>
-  Finally, you can search and install programs:
-<screen>
-<prompt>$ </prompt>flatpak search bustle
-<prompt>$ </prompt>flatpak install flathub org.freedesktop.Bustle
-<prompt>$ </prompt>flatpak run org.freedesktop.Bustle
-</screen>
-  Again, GNOME Software offers graphical interface for these tasks.
- </para>
-</chapter>
diff --git a/nixos/modules/services/desktops/gnome/evolution-data-server.nix b/nixos/modules/services/desktops/gnome/evolution-data-server.nix
index 0006ba1a7ba..a8db7dce8fd 100644
--- a/nixos/modules/services/desktops/gnome/evolution-data-server.nix
+++ b/nixos/modules/services/desktops/gnome/evolution-data-server.nix
@@ -27,7 +27,7 @@ with lib;
   options = {
 
     services.gnome.evolution-data-server = {
-      enable = mkEnableOption (lib.mdDoc "Evolution Data Server, a collection of services for storing addressbooks and calendars.");
+      enable = mkEnableOption (lib.mdDoc "Evolution Data Server, a collection of services for storing addressbooks and calendars");
       plugins = mkOption {
         type = types.listOf types.package;
         default = [ ];
@@ -35,7 +35,7 @@ with lib;
       };
     };
     programs.evolution = {
-      enable = mkEnableOption (lib.mdDoc "Evolution, a Personal information management application that provides integrated mail, calendaring and address book functionality.");
+      enable = mkEnableOption (lib.mdDoc "Evolution, a Personal information management application that provides integrated mail, calendaring and address book functionality");
       plugins = mkOption {
         type = types.listOf types.package;
         default = [ ];
diff --git a/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json b/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json
deleted file mode 100644
index 9aa51b61431..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
-  "context.properties": {
-    "log.level": 0
-  },
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "support.*": "support/libspa-support"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-rt",
-      "args": {},
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-client-device"
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-metadata"
-    },
-    {
-      "name": "libpipewire-module-session-manager"
-    }
-  ],
-  "filter.properties": {},
-  "stream.properties": {}
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/client.conf.json b/nixos/modules/services/desktops/pipewire/daemon/client.conf.json
deleted file mode 100644
index 71294a0e78a..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/client.conf.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
-  "context.properties": {
-    "log.level": 0
-  },
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "support.*": "support/libspa-support"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-client-device"
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-metadata"
-    },
-    {
-      "name": "libpipewire-module-session-manager"
-    }
-  ],
-  "filter.properties": {},
-  "stream.properties": {}
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json b/nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json
deleted file mode 100644
index 689fca88359..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
-  "context.properties": {
-    "log.level": 0
-  },
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "support.*": "support/libspa-support"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-rt",
-      "args": {},
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    }
-  ]
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/jack.conf.json b/nixos/modules/services/desktops/pipewire/daemon/jack.conf.json
deleted file mode 100644
index 4a173f73229..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/jack.conf.json
+++ /dev/null
@@ -1,63 +0,0 @@
-{
-  "context.properties": {
-    "log.level": 0
-  },
-  "context.spa-libs": {
-    "support.*": "support/libspa-support"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-rt",
-      "args": {},
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-metadata"
-    }
-  ],
-  "jack.properties": {},
-  "jack.rules": [
-    {
-      "matches": [
-        {}
-      ],
-      "actions": {
-        "update-props": {}
-      }
-    },
-    {
-      "matches": [
-        {
-          "application.process.binary": "jack_bufsize"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "jack.global-buffer-size": true
-        }
-      }
-    },
-    {
-      "matches": [
-        {
-          "application.process.binary": "qsynth"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "node.pause-on-idle": false,
-          "node.passive": true
-        }
-      }
-    }
-  ]
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json b/nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json
deleted file mode 100644
index 0f1ebe5749c..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json
+++ /dev/null
@@ -1,120 +0,0 @@
-{
-  "context.properties": {
-    "link.max-buffers": 16,
-    "core.daemon": true,
-    "core.name": "pipewire-0",
-    "settings.check-quantum": true,
-    "settings.check-rate": true,
-    "vm.overrides": {
-      "default.clock.min-quantum": 1024
-    }
-  },
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "api.alsa.*": "alsa/libspa-alsa",
-    "support.*": "support/libspa-support"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-rt",
-      "args": {
-        "nice.level": -11
-      },
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-profiler"
-    },
-    {
-      "name": "libpipewire-module-metadata"
-    },
-    {
-      "name": "libpipewire-module-spa-node-factory"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-access",
-      "args": {}
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-link-factory"
-    }
-  ],
-  "context.objects": [
-    {
-      "factory": "metadata",
-      "args": {
-        "metadata.name": "default"
-      }
-    },
-    {
-      "factory": "spa-node-factory",
-      "args": {
-        "factory.name": "support.node.driver",
-        "node.name": "Dummy-Driver",
-        "node.group": "pipewire.dummy",
-        "priority.driver": 20000
-      }
-    },
-    {
-      "factory": "spa-node-factory",
-      "args": {
-        "factory.name": "support.node.driver",
-        "node.name": "Freewheel-Driver",
-        "priority.driver": 19000,
-        "node.group": "pipewire.freewheel",
-        "node.freewheel": true
-      }
-    },
-    {
-      "factory": "adapter",
-      "args": {
-        "factory.name": "api.alsa.pcm.source",
-        "node.name": "system",
-        "node.description": "system",
-        "media.class": "Audio/Source",
-        "api.alsa.path": "hw:0",
-        "node.suspend-on-idle": true,
-        "resample.disable": true,
-        "channelmix.disable": true,
-        "adapter.auto-port-config": {
-          "mode": "dsp",
-          "monitor": false,
-          "control": false,
-          "position": "unknown"
-        }
-      }
-    },
-    {
-      "factory": "adapter",
-      "args": {
-        "factory.name": "api.alsa.pcm.sink",
-        "node.name": "system",
-        "node.description": "system",
-        "media.class": "Audio/Sink",
-        "api.alsa.path": "hw:0",
-        "node.suspend-on-idle": true,
-        "resample.disable": true,
-        "channelmix.disable": true,
-        "adapter.auto-port-config": {
-          "mode": "dsp",
-          "monitor": false,
-          "control": false,
-          "position": "unknown"
-        }
-      }
-    }
-  ],
-  "context.exec": []
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json b/nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json
deleted file mode 100644
index 4f669895d87..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "context.properties": {},
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "support.*": "support/libspa-support"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-rt",
-      "args": {
-        "nice.level": -11
-      },
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-avb",
-      "args": {}
-    }
-  ],
-  "context.exec": [],
-  "stream.properties": {},
-  "avb.properties": {
-    "ifname": "enp3s0",
-    "vm.overrides": {}
-  }
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json b/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json
deleted file mode 100644
index b1a86485332..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json
+++ /dev/null
@@ -1,106 +0,0 @@
-{
-  "context.properties": {},
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "support.*": "support/libspa-support"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-rt",
-      "args": {
-        "nice.level": -11
-      },
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-metadata"
-    },
-    {
-      "name": "libpipewire-module-protocol-pulse",
-      "args": {}
-    }
-  ],
-  "context.exec": [],
-  "pulse.cmd": [
-    {
-      "cmd": "load-module",
-      "args": "module-always-sink",
-      "flags": []
-    }
-  ],
-  "stream.properties": {},
-  "pulse.properties": {
-    "server.address": [
-      "unix:native"
-    ],
-    "vm.overrides": {
-      "pulse.min.quantum": "1024/48000"
-    }
-  },
-  "pulse.rules": [
-    {
-      "matches": [
-        {}
-      ],
-      "actions": {
-        "update-props": {}
-      }
-    },
-    {
-      "matches": [
-        {
-          "application.process.binary": "teams"
-        },
-        {
-          "application.process.binary": "teams-insiders"
-        },
-        {
-          "application.process.binary": "skypeforlinux"
-        }
-      ],
-      "actions": {
-        "quirks": [
-          "force-s16-info"
-        ]
-      }
-    },
-    {
-      "matches": [
-        {
-          "application.process.binary": "firefox"
-        }
-      ],
-      "actions": {
-        "quirks": [
-          "remove-capture-dont-move"
-        ]
-      }
-    },
-    {
-      "matches": [
-        {
-          "application.name": "~speech-dispatcher.*"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "pulse.min.req": "512/48000",
-          "pulse.min.quantum": "512/48000",
-          "pulse.idle.timeout": 5
-        }
-      }
-    }
-  ]
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json b/nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json
deleted file mode 100644
index 53fc103d221..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json
+++ /dev/null
@@ -1,105 +0,0 @@
-{
-  "context.properties": {
-    "link.max-buffers": 16,
-    "core.daemon": true,
-    "core.name": "pipewire-0",
-    "default.clock.min-quantum": 16,
-    "vm.overrides": {
-      "default.clock.min-quantum": 1024
-    }
-  },
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "avb.*": "avb/libspa-avb",
-    "api.alsa.*": "alsa/libspa-alsa",
-    "api.v4l2.*": "v4l2/libspa-v4l2",
-    "api.libcamera.*": "libcamera/libspa-libcamera",
-    "api.bluez5.*": "bluez5/libspa-bluez5",
-    "api.vulkan.*": "vulkan/libspa-vulkan",
-    "api.jack.*": "jack/libspa-jack",
-    "support.*": "support/libspa-support"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-rt",
-      "args": {
-        "nice.level": -11
-      },
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-profiler"
-    },
-    {
-      "name": "libpipewire-module-metadata"
-    },
-    {
-      "name": "libpipewire-module-spa-device-factory"
-    },
-    {
-      "name": "libpipewire-module-spa-node-factory"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-client-device"
-    },
-    {
-      "name": "libpipewire-module-portal",
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-access",
-      "args": {}
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-link-factory"
-    },
-    {
-      "name": "libpipewire-module-session-manager"
-    },
-    {
-      "name": "libpipewire-module-x11-bell",
-      "args": {},
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    }
-  ],
-  "context.objects": [
-    {
-      "factory": "spa-node-factory",
-      "args": {
-        "factory.name": "support.node.driver",
-        "node.name": "Dummy-Driver",
-        "node.group": "pipewire.dummy",
-        "priority.driver": 20000
-      }
-    },
-    {
-      "factory": "spa-node-factory",
-      "args": {
-        "factory.name": "support.node.driver",
-        "node.name": "Freewheel-Driver",
-        "priority.driver": 19000,
-        "node.group": "pipewire.freewheel",
-        "node.freewheel": true
-      }
-    }
-  ],
-  "context.exec": []
-}
diff --git a/nixos/modules/services/desktops/pipewire/media-session/alsa-monitor.conf.json b/nixos/modules/services/desktops/pipewire/media-session/alsa-monitor.conf.json
deleted file mode 100644
index 53fc9cc9634..00000000000
--- a/nixos/modules/services/desktops/pipewire/media-session/alsa-monitor.conf.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
-  "properties": {},
-  "rules": [
-    {
-      "matches": [
-        {
-          "device.name": "~alsa_card.*"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "api.alsa.use-acp": true,
-          "api.acp.auto-profile": false,
-          "api.acp.auto-port": false
-        }
-      }
-    },
-    {
-      "matches": [
-        {
-          "node.name": "~alsa_input.*"
-        },
-        {
-          "node.name": "~alsa_output.*"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "node.pause-on-idle": false
-        }
-      }
-    }
-  ]
-}
diff --git a/nixos/modules/services/desktops/pipewire/media-session/bluez-monitor.conf.json b/nixos/modules/services/desktops/pipewire/media-session/bluez-monitor.conf.json
deleted file mode 100644
index 6d1c23e8256..00000000000
--- a/nixos/modules/services/desktops/pipewire/media-session/bluez-monitor.conf.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
-  "properties": {},
-  "rules": [
-    {
-      "matches": [
-        {
-          "device.name": "~bluez_card.*"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "bluez5.auto-connect": [
-            "hfp_hf",
-            "hsp_hs",
-            "a2dp_sink"
-          ]
-        }
-      }
-    },
-    {
-      "matches": [
-        {
-          "node.name": "~bluez_input.*"
-        },
-        {
-          "node.name": "~bluez_output.*"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "node.pause-on-idle": false
-        }
-      }
-    }
-  ]
-}
diff --git a/nixos/modules/services/desktops/pipewire/media-session/media-session.conf.json b/nixos/modules/services/desktops/pipewire/media-session/media-session.conf.json
deleted file mode 100644
index 4b4e302af38..00000000000
--- a/nixos/modules/services/desktops/pipewire/media-session/media-session.conf.json
+++ /dev/null
@@ -1,68 +0,0 @@
-{
-  "context.properties": {},
-  "context.spa-libs": {
-    "api.bluez5.*": "bluez5/libspa-bluez5",
-    "api.alsa.*": "alsa/libspa-alsa",
-    "api.v4l2.*": "v4l2/libspa-v4l2",
-    "api.libcamera.*": "libcamera/libspa-libcamera"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-rtkit",
-      "args": {},
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-client-device"
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-metadata"
-    },
-    {
-      "name": "libpipewire-module-session-manager"
-    }
-  ],
-  "session.modules": {
-    "default": [
-      "flatpak",
-      "portal",
-      "v4l2",
-      "suspend-node",
-      "policy-node"
-    ],
-    "with-audio": [
-      "metadata",
-      "default-nodes",
-      "default-profile",
-      "default-routes",
-      "alsa-seq",
-      "alsa-monitor"
-    ],
-    "with-alsa": [
-      "with-audio"
-    ],
-    "with-jack": [
-      "with-audio"
-    ],
-    "with-pulseaudio": [
-      "with-audio",
-      "bluez5",
-      "bluez5-autoswitch",
-      "logind",
-      "restore-stream",
-      "streams-follow-default"
-    ]
-  }
-}
diff --git a/nixos/modules/services/desktops/pipewire/media-session/v4l2-monitor.conf.json b/nixos/modules/services/desktops/pipewire/media-session/v4l2-monitor.conf.json
deleted file mode 100644
index b08cba1b604..00000000000
--- a/nixos/modules/services/desktops/pipewire/media-session/v4l2-monitor.conf.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
-  "properties": {},
-  "rules": [
-    {
-      "matches": [
-        {
-          "device.name": "~v4l2_device.*"
-        }
-      ],
-      "actions": {
-        "update-props": {}
-      }
-    },
-    {
-      "matches": [
-        {
-          "node.name": "~v4l2_input.*"
-        },
-        {
-          "node.name": "~v4l2_output.*"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "node.pause-on-idle": false
-        }
-      }
-    }
-  ]
-}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix b/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
deleted file mode 100644
index 203139294c6..00000000000
--- a/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
+++ /dev/null
@@ -1,141 +0,0 @@
-# pipewire example session manager.
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  json = pkgs.formats.json {};
-  cfg = config.services.pipewire.media-session;
-  enable32BitAlsaPlugins = cfg.alsa.support32Bit
-                           && pkgs.stdenv.isx86_64
-                           && pkgs.pkgsi686Linux.pipewire != null;
-
-  # Use upstream config files passed through spa-json-dump as the base
-  # Patched here as necessary for them to work with this module
-  defaults = {
-    alsa-monitor = lib.importJSON ./media-session/alsa-monitor.conf.json;
-    bluez-monitor = lib.importJSON ./media-session/bluez-monitor.conf.json;
-    media-session = lib.importJSON ./media-session/media-session.conf.json;
-    v4l2-monitor = lib.importJSON ./media-session/v4l2-monitor.conf.json;
-  };
-
-  configs = {
-    alsa-monitor = recursiveUpdate defaults.alsa-monitor cfg.config.alsa-monitor;
-    bluez-monitor = recursiveUpdate defaults.bluez-monitor cfg.config.bluez-monitor;
-    media-session = recursiveUpdate defaults.media-session cfg.config.media-session;
-    v4l2-monitor = recursiveUpdate defaults.v4l2-monitor cfg.config.v4l2-monitor;
-  };
-in {
-
-  meta = {
-    maintainers = teams.freedesktop.members;
-    # uses attributes of the linked package
-    buildDocsInSandbox = false;
-  };
-
-  ###### interface
-  options = {
-    services.pipewire.media-session = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Whether to enable the deprecated example Pipewire session manager";
-      };
-
-      package = mkOption {
-        type = types.package;
-        default = pkgs.pipewire-media-session;
-        defaultText = literalExpression "pkgs.pipewire-media-session";
-        description = lib.mdDoc ''
-          The pipewire-media-session derivation to use.
-        '';
-      };
-
-      config = {
-        media-session = mkOption {
-          type = json.type;
-          description = lib.mdDoc ''
-            Configuration for the media session core. For details see
-            https://gitlab.freedesktop.org/pipewire/media-session/-/blob/${cfg.package.version}/src/daemon/media-session.d/media-session.conf
-          '';
-          default = defaults.media-session;
-        };
-
-        alsa-monitor = mkOption {
-          type = json.type;
-          description = lib.mdDoc ''
-            Configuration for the alsa monitor. For details see
-            https://gitlab.freedesktop.org/pipewire/media-session/-/blob/${cfg.package.version}/src/daemon/media-session.d/alsa-monitor.conf
-          '';
-          default = defaults.alsa-monitor;
-        };
-
-        bluez-monitor = mkOption {
-          type = json.type;
-          description = lib.mdDoc ''
-            Configuration for the bluez5 monitor. For details see
-            https://gitlab.freedesktop.org/pipewire/media-session/-/blob/${cfg.package.version}/src/daemon/media-session.d/bluez-monitor.conf
-          '';
-          default = defaults.bluez-monitor;
-        };
-
-        v4l2-monitor = mkOption {
-          type = json.type;
-          description = lib.mdDoc ''
-            Configuration for the V4L2 monitor. For details see
-            https://gitlab.freedesktop.org/pipewire/media-session/-/blob/${cfg.package.version}/src/daemon/media-session.d/v4l2-monitor.conf
-          '';
-          default = defaults.v4l2-monitor;
-        };
-      };
-    };
-  };
-
-  ###### implementation
-  config = mkIf cfg.enable {
-    environment.systemPackages = [ cfg.package ];
-    systemd.packages = [ cfg.package ];
-
-    # Enable either system or user units.
-    systemd.services.pipewire-media-session.enable = config.services.pipewire.systemWide;
-    systemd.user.services.pipewire-media-session.enable = !config.services.pipewire.systemWide;
-
-    systemd.services.pipewire-media-session.wantedBy = [ "pipewire.service" ];
-    systemd.user.services.pipewire-media-session.wantedBy = [ "pipewire.service" ];
-
-    environment.etc."pipewire/media-session.d/media-session.conf" = {
-      source = json.generate "media-session.conf" configs.media-session;
-    };
-    environment.etc."pipewire/media-session.d/v4l2-monitor.conf" = {
-      source = json.generate "v4l2-monitor.conf" configs.v4l2-monitor;
-    };
-
-    environment.etc."pipewire/media-session.d/with-audio" =
-      mkIf config.services.pipewire.audio.enable {
-        text = "";
-      };
-
-    environment.etc."pipewire/media-session.d/with-alsa" =
-      mkIf config.services.pipewire.alsa.enable {
-        text = "";
-      };
-    environment.etc."pipewire/media-session.d/alsa-monitor.conf" =
-      mkIf config.services.pipewire.alsa.enable {
-        source = json.generate "alsa-monitor.conf" configs.alsa-monitor;
-      };
-
-    environment.etc."pipewire/media-session.d/with-pulseaudio" =
-      mkIf config.services.pipewire.pulse.enable {
-        text = "";
-      };
-    environment.etc."pipewire/media-session.d/bluez-monitor.conf" =
-      mkIf config.services.pipewire.pulse.enable {
-        source = json.generate "bluez-monitor.conf" configs.bluez-monitor;
-      };
-
-    environment.etc."pipewire/media-session.d/with-jack" =
-      mkIf config.services.pipewire.jack.enable {
-        text = "";
-      };
-  };
-}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire.nix b/nixos/modules/services/desktops/pipewire/pipewire.nix
index a4ef88a45ad..ae695baf42c 100644
--- a/nixos/modules/services/desktops/pipewire/pipewire.nix
+++ b/nixos/modules/services/desktops/pipewire/pipewire.nix
@@ -4,7 +4,6 @@
 with lib;
 
 let
-  json = pkgs.formats.json {};
   cfg = config.services.pipewire;
   enable32BitAlsaPlugins = cfg.alsa.support32Bit
                            && pkgs.stdenv.isx86_64
@@ -18,34 +17,8 @@ let
     mkdir -p "$out/lib"
     ln -s "${cfg.package.jack}/lib" "$out/lib/pipewire"
   '';
-
-  # Use upstream config files passed through spa-json-dump as the base
-  # Patched here as necessary for them to work with this module
-  defaults = {
-    client = lib.importJSON ./daemon/client.conf.json;
-    client-rt = lib.importJSON ./daemon/client-rt.conf.json;
-    jack = lib.importJSON ./daemon/jack.conf.json;
-    minimal = lib.importJSON ./daemon/minimal.conf.json;
-    pipewire = lib.importJSON ./daemon/pipewire.conf.json;
-    pipewire-pulse = lib.importJSON ./daemon/pipewire-pulse.conf.json;
-  };
-
-  useSessionManager = cfg.wireplumber.enable || cfg.media-session.enable;
-
-  configs = {
-    client = recursiveUpdate defaults.client cfg.config.client;
-    client-rt = recursiveUpdate defaults.client-rt cfg.config.client-rt;
-    jack = recursiveUpdate defaults.jack cfg.config.jack;
-    pipewire = recursiveUpdate (if useSessionManager then defaults.pipewire else defaults.minimal) cfg.config.pipewire;
-    pipewire-pulse = recursiveUpdate defaults.pipewire-pulse cfg.config.pipewire-pulse;
-  };
 in {
-
-  meta = {
-    maintainers = teams.freedesktop.members;
-    # uses attributes of the linked package
-    buildDocsInSandbox = false;
-  };
+  meta.maintainers = teams.freedesktop.members ++ [ lib.maintainers.k900 ];
 
   ###### interface
   options = {
@@ -69,53 +42,6 @@ in {
         '';
       };
 
-      config = {
-        client = mkOption {
-          type = json.type;
-          default = {};
-          description = lib.mdDoc ''
-            Configuration for pipewire clients. For details see
-            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/client.conf.in
-          '';
-        };
-
-        client-rt = mkOption {
-          type = json.type;
-          default = {};
-          description = lib.mdDoc ''
-            Configuration for realtime pipewire clients. For details see
-            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/client-rt.conf.in
-          '';
-        };
-
-        jack = mkOption {
-          type = json.type;
-          default = {};
-          description = lib.mdDoc ''
-            Configuration for the pipewire daemon's jack module. For details see
-            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/jack.conf.in
-          '';
-        };
-
-        pipewire = mkOption {
-          type = json.type;
-          default = {};
-          description = lib.mdDoc ''
-            Configuration for the pipewire daemon. For details see
-            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/pipewire.conf.in
-          '';
-        };
-
-        pipewire-pulse = mkOption {
-          type = json.type;
-          default = {};
-          description = lib.mdDoc ''
-            Configuration for the pipewire-pulse daemon. For details see
-            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/pipewire-pulse.conf.in
-          '';
-        };
-      };
-
       audio = {
         enable = lib.mkOption {
           type = lib.types.bool;
@@ -153,10 +79,20 @@ in {
           https://github.com/PipeWire/pipewire/blob/master/NEWS
         '';
       };
-
     };
   };
 
+  imports = [
+    (lib.mkRemovedOptionModule ["services" "pipewire" "config"] ''
+      Overriding default Pipewire configuration through NixOS options never worked correctly and is no longer supported.
+      Please create drop-in files in /etc/pipewire/pipewire.conf.d/ to make the desired setting changes instead.
+    '')
+
+    (lib.mkRemovedOptionModule ["services" "pipewire" "media-session"] ''
+      pipewire-media-session is no longer supported upstream and has been removed.
+      Please switch to `services.pipewire.wireplumber` instead.
+    '')
+  ];
 
   ###### implementation
   config = mkIf cfg.enable {
@@ -222,22 +158,6 @@ in {
       source = "${cfg.package}/share/alsa/alsa.conf.d/99-pipewire-default.conf";
     };
 
-    environment.etc."pipewire/client.conf" = {
-      source = json.generate "client.conf" configs.client;
-    };
-    environment.etc."pipewire/client-rt.conf" = {
-      source = json.generate "client-rt.conf" configs.client-rt;
-    };
-    environment.etc."pipewire/jack.conf" = {
-      source = json.generate "jack.conf" configs.jack;
-    };
-    environment.etc."pipewire/pipewire.conf" = {
-      source = json.generate "pipewire.conf" configs.pipewire;
-    };
-    environment.etc."pipewire/pipewire-pulse.conf" = mkIf cfg.pulse.enable {
-      source = json.generate "pipewire-pulse.conf" configs.pipewire-pulse;
-    };
-
     environment.sessionVariables.LD_LIBRARY_PATH =
       lib.mkIf cfg.jack.enable [ "${cfg.package.jack}/lib" ];
 
@@ -256,12 +176,5 @@ in {
       };
       groups.pipewire.gid = config.ids.gids.pipewire;
     };
-
-    # https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/464#note_723554
-    systemd.services.pipewire.environment."PIPEWIRE_LINK_PASSIVE" = "1";
-    systemd.user.services.pipewire.environment."PIPEWIRE_LINK_PASSIVE" = "1";
-
-    # pipewire-pulse default config expects pactl to be in PATH
-    systemd.user.services.pipewire-pulse.path = lib.mkIf cfg.pulse.enable [ pkgs.pulseaudio ];
   };
 }
diff --git a/nixos/modules/services/desktops/pipewire/wireplumber.nix b/nixos/modules/services/desktops/pipewire/wireplumber.nix
index 4b36b99aa7c..95a7ece26c5 100644
--- a/nixos/modules/services/desktops/pipewire/wireplumber.nix
+++ b/nixos/modules/services/desktops/pipewire/wireplumber.nix
@@ -29,10 +29,6 @@ in
   config = lib.mkIf cfg.enable {
     assertions = [
       {
-        assertion = !config.services.pipewire.media-session.enable;
-        message = "WirePlumber and pipewire-media-session can't be enabled at the same time.";
-      }
-      {
         assertion = !config.hardware.bluetooth.hsphfpd.enable;
         message = "Using Wireplumber conflicts with hsphfpd, as it provides the same functionality. `hardware.bluetooth.hsphfpd.enable` needs be set to false";
       }
diff --git a/nixos/modules/services/desktops/system76-scheduler.nix b/nixos/modules/services/desktops/system76-scheduler.nix
new file mode 100644
index 00000000000..267b528cc5d
--- /dev/null
+++ b/nixos/modules/services/desktops/system76-scheduler.nix
@@ -0,0 +1,296 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.system76-scheduler;
+
+  inherit (builtins) concatStringsSep map toString attrNames;
+  inherit (lib) boolToString types mkOption literalExpression mdDoc optional mkIf mkMerge;
+  inherit (types) nullOr listOf bool int ints float str enum;
+
+  withDefaults = optionSpecs: defaults:
+    lib.genAttrs (attrNames optionSpecs) (name:
+      mkOption (optionSpecs.${name} // {
+        default = optionSpecs.${name}.default or defaults.${name} or null;
+      }));
+
+  latencyProfile = withDefaults {
+    latency = {
+      type = int;
+      description = mdDoc "`sched_latency_ns`.";
+    };
+    nr-latency = {
+      type = int;
+      description = mdDoc "`sched_nr_latency`.";
+    };
+    wakeup-granularity = {
+      type = float;
+      description = mdDoc "`sched_wakeup_granularity_ns`.";
+    };
+    bandwidth-size = {
+      type = int;
+      description = mdDoc "`sched_cfs_bandwidth_slice_us`.";
+    };
+    preempt = {
+      type = enum [ "none" "voluntary" "full" ];
+      description = mdDoc "Preemption mode.";
+    };
+  };
+  schedulerProfile = withDefaults {
+    nice = {
+      type = nullOr (ints.between (-20) 19);
+      description = mdDoc "Niceness.";
+    };
+    class = {
+      type = nullOr (enum [ "idle" "batch" "other" "rr" "fifo" ]);
+      example = literalExpression "\"batch\"";
+      description = mdDoc "CPU scheduler class.";
+    };
+    prio = {
+      type = nullOr (ints.between 1 99);
+      example = literalExpression "49";
+      description = mdDoc "CPU scheduler priority.";
+    };
+    ioClass = {
+      type = nullOr (enum [ "idle" "best-effort" "realtime" ]);
+      example = literalExpression "\"best-effort\"";
+      description = mdDoc "IO scheduler class.";
+    };
+    ioPrio = {
+      type = nullOr (ints.between 0 7);
+      example = literalExpression "4";
+      description = mdDoc "IO scheduler priority.";
+    };
+    matchers = {
+      type = nullOr (listOf str);
+      default = [];
+      example = literalExpression ''
+        [
+          "include cgroup=\"/user.slice/*.service\" parent=\"systemd\""
+          "emacs"
+        ]
+      '';
+      description = mdDoc "Process matchers.";
+    };
+  };
+
+  cfsProfileToString = name: let
+    p = cfg.settings.cfsProfiles.${name};
+  in
+    "${name} latency=${toString p.latency} nr-latency=${toString p.nr-latency} wakeup-granularity=${toString p.wakeup-granularity} bandwidth-size=${toString p.bandwidth-size} preempt=\"${p.preempt}\"";
+
+  prioToString = class: prio: if prio == null then "\"${class}\"" else "(${class})${toString prio}";
+
+  schedulerProfileToString = name: a: indent:
+    concatStringsSep " "
+      (["${indent}${name}"]
+       ++ (optional (a.nice != null) "nice=${toString a.nice}")
+       ++ (optional (a.class != null) "sched=${prioToString a.class a.prio}")
+       ++ (optional (a.ioClass != null) "io=${prioToString a.ioClass a.ioPrio}")
+       ++ (optional ((builtins.length a.matchers) != 0) ("{\n${concatStringsSep "\n" (map (m: "  ${indent}${m}") a.matchers)}\n${indent}}")));
+
+in {
+  options = {
+    services.system76-scheduler = {
+      enable = lib.mkEnableOption (lib.mdDoc "system76-scheduler");
+
+      package = mkOption {
+        type = types.package;
+        default = config.boot.kernelPackages.system76-scheduler;
+        defaultText = literalExpression "config.boot.kernelPackages.system76-scheduler";
+        description = mdDoc "Which System76-Scheduler package to use.";
+      };
+
+      useStockConfig = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Use the (reasonable and featureful) stock configuration.
+
+          When this option is `true`, `services.system76-scheduler.settings`
+          are ignored.
+        '';
+      };
+
+      settings = {
+        cfsProfiles = {
+          enable = mkOption {
+            type = bool;
+            default = true;
+            description = mdDoc "Tweak CFS latency parameters when going on/off battery";
+          };
+
+          default = latencyProfile {
+            latency = 6;
+            nr-latency = 8;
+            wakeup-granularity = 1.0;
+            bandwidth-size = 5;
+            preempt = "voluntary";
+          };
+          responsive = latencyProfile {
+            latency = 4;
+            nr-latency = 10;
+            wakeup-granularity = 0.5;
+            bandwidth-size = 3;
+            preempt = "full";
+          };
+        };
+
+        processScheduler = {
+          enable = mkOption {
+            type = bool;
+            default = true;
+            description = mdDoc "Tweak scheduling of individual processes in real time.";
+          };
+
+          useExecsnoop = mkOption {
+            type = bool;
+            default = true;
+            description = mdDoc "Use execsnoop (otherwise poll the precess list periodically).";
+          };
+
+          refreshInterval = mkOption {
+            type = int;
+            default = 60;
+            description = mdDoc "Process list poll interval, in seconds";
+          };
+
+          foregroundBoost = {
+            enable = mkOption {
+              type = bool;
+              default = true;
+              description = mdDoc ''
+                Boost foreground process priorities.
+
+                (And de-boost background ones).  Note that this option needs cooperation
+                from the desktop environment to work.  On Gnome the client side is
+                implemented by the "System76 Scheduler" shell extension.
+              '';
+            };
+            foreground = schedulerProfile {
+              nice = 0;
+              ioClass = "best-effort";
+              ioPrio = 0;
+            };
+            background = schedulerProfile {
+              nice = 6;
+              ioClass = "idle";
+            };
+          };
+
+          pipewireBoost = {
+            enable = mkOption {
+              type = bool;
+              default = true;
+              description = mdDoc "Boost Pipewire client priorities.";
+            };
+            profile = schedulerProfile {
+              nice = -6;
+              ioClass = "best-effort";
+              ioPrio = 0;
+            };
+          };
+        };
+      };
+
+      assignments = mkOption {
+        type = types.attrsOf (types.submodule {
+          options = schedulerProfile { };
+        });
+        default = {};
+        example = literalExpression ''
+          {
+            nix-builds = {
+              nice = 15;
+              class = "batch";
+              ioClass = "idle";
+              matchers = [
+                "nix-daemon"
+              ];
+            };
+          }
+        '';
+        description = mdDoc "Process profile assignments.";
+      };
+
+      exceptions = mkOption {
+        type = types.listOf str;
+        default = [];
+        example = literalExpression ''
+          [
+            "include descends=\"schedtool\""
+            "schedtool"
+          ]
+        '';
+        description = mdDoc "Processes that are left alone.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    services.dbus.packages = [ cfg.package ];
+
+    systemd.services.system76-scheduler = {
+      description = "Manage process priorities and CFS scheduler latencies for improved responsiveness on the desktop";
+      wantedBy = [ "multi-user.target" ];
+      path = [
+        # execsnoop needs those to extract kernel headers:
+        pkgs.kmod
+        pkgs.gnutar
+        pkgs.xz
+      ];
+      serviceConfig = {
+        Type = "dbus";
+        BusName= "com.system76.Scheduler";
+        ExecStart = "${cfg.package}/bin/system76-scheduler daemon";
+        ExecReload = "${cfg.package}/bin/system76-scheduler daemon reload";
+      };
+    };
+
+    environment.etc = mkMerge [
+      (mkIf cfg.useStockConfig {
+        # No custom settings: just use stock configuration with a fix for Pipewire
+        "system76-scheduler/config.kdl".source = "${cfg.package}/data/config.kdl";
+        "system76-scheduler/process-scheduler/00-dist.kdl".source = "${cfg.package}/data/pop_os.kdl";
+        "system76-scheduler/process-scheduler/01-fix-pipewire-paths.kdl".source = ../../../../pkgs/os-specific/linux/system76-scheduler/01-fix-pipewire-paths.kdl;
+      })
+
+      (let
+        settings = cfg.settings;
+        cfsp = settings.cfsProfiles;
+        ps = settings.processScheduler;
+      in mkIf (!cfg.useStockConfig) {
+        "system76-scheduler/config.kdl".text = ''
+          version "2.0"
+          autogroup-enabled false
+          cfs-profiles enable=${boolToString cfsp.enable} {
+            ${cfsProfileToString "default"}
+            ${cfsProfileToString "responsive"}
+          }
+          process-scheduler enable=${boolToString ps.enable} {
+            execsnoop ${boolToString ps.useExecsnoop}
+            refresh-rate ${toString ps.refreshInterval}
+            assignments {
+              ${if ps.foregroundBoost.enable then (schedulerProfileToString "foreground" ps.foregroundBoost.foreground "    ") else ""}
+              ${if ps.foregroundBoost.enable then (schedulerProfileToString "background" ps.foregroundBoost.background "    ") else ""}
+              ${if ps.pipewireBoost.enable then (schedulerProfileToString "pipewire" ps.pipewireBoost.profile "    ") else ""}
+            }
+          }
+        '';
+      })
+
+      {
+        "system76-scheduler/process-scheduler/02-config.kdl".text =
+          "exceptions {\n${concatStringsSep "\n" (map (e: "  ${e}") cfg.exceptions)}\n}\n"
+          + "assignments {\n"
+          + (concatStringsSep "\n" (map (name: schedulerProfileToString name cfg.assignments.${name} "  ")
+            (attrNames cfg.assignments)))
+          + "\n}\n";
+      }
+    ];
+  };
+
+  meta = {
+    maintainers = [ lib.maintainers.cmm ];
+  };
+}
diff --git a/nixos/modules/services/development/blackfire.md b/nixos/modules/services/development/blackfire.md
new file mode 100644
index 00000000000..e2e7e4780c7
--- /dev/null
+++ b/nixos/modules/services/development/blackfire.md
@@ -0,0 +1,39 @@
+# Blackfire profiler {#module-services-blackfire}
+
+*Source:* {file}`modules/services/development/blackfire.nix`
+
+*Upstream documentation:* <https://blackfire.io/docs/introduction>
+
+[Blackfire](https://blackfire.io) is a proprietary tool for profiling applications. There are several languages supported by the product but currently only PHP support is packaged in Nixpkgs. The back-end consists of a module that is loaded into the language runtime (called *probe*) and a service (*agent*) that the probe connects to and that sends the profiles to the server.
+
+To use it, you will need to enable the agent and the probe on your server. The exact method will depend on the way you use PHP but here is an example of NixOS configuration for PHP-FPM:
+```
+let
+  php = pkgs.php.withExtensions ({ enabled, all }: enabled ++ (with all; [
+    blackfire
+  ]));
+in {
+  # Enable the probe extension for PHP-FPM.
+  services.phpfpm = {
+    phpPackage = php;
+  };
+
+  # Enable and configure the agent.
+  services.blackfire-agent = {
+    enable = true;
+    settings = {
+      # You will need to get credentials at https://blackfire.io/my/settings/credentials
+      # You can also use other options described in https://blackfire.io/docs/up-and-running/configuration/agent
+      server-id = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
+      server-token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
+    };
+  };
+
+  # Make the agent run on start-up.
+  # (WantedBy= from the upstream unit not respected: https://github.com/NixOS/nixpkgs/issues/81138)
+  # Alternately, you can start it manually with `systemctl start blackfire-agent`.
+  systemd.services.blackfire-agent.wantedBy = [ "phpfpm-foo.service" ];
+}
+```
+
+On your developer machine, you will also want to install [the client](https://blackfire.io/docs/up-and-running/installation#install-a-profiling-client) (see `blackfire` package) or the browser extension to actually trigger the profiling.
diff --git a/nixos/modules/services/development/blackfire.nix b/nixos/modules/services/development/blackfire.nix
index 054cef9ae80..3c98d7a281c 100644
--- a/nixos/modules/services/development/blackfire.nix
+++ b/nixos/modules/services/development/blackfire.nix
@@ -11,7 +11,7 @@ let
 in {
   meta = {
     maintainers = pkgs.blackfire.meta.maintainers;
-    doc = ./blackfire.xml;
+    doc = ./blackfire.md;
   };
 
   options = {
diff --git a/nixos/modules/services/development/blackfire.xml b/nixos/modules/services/development/blackfire.xml
deleted file mode 100644
index cecd249dda4..00000000000
--- a/nixos/modules/services/development/blackfire.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:id="module-services-blackfire">
- <title>Blackfire profiler</title>
- <para>
-  <emphasis>Source:</emphasis>
-  <filename>modules/services/development/blackfire.nix</filename>
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="https://blackfire.io/docs/introduction"/>
- </para>
- <para>
-  <link xlink:href="https://blackfire.io">Blackfire</link> is a proprietary tool for profiling applications. There are several languages supported by the product but currently only PHP support is packaged in Nixpkgs. The back-end consists of a module that is loaded into the language runtime (called <firstterm>probe</firstterm>) and a service (<firstterm>agent</firstterm>) that the probe connects to and that sends the profiles to the server.
- </para>
- <para>
-  To use it, you will need to enable the agent and the probe on your server. The exact method will depend on the way you use PHP but here is an example of NixOS configuration for PHP-FPM:
-<programlisting>let
-  php = pkgs.php.withExtensions ({ enabled, all }: enabled ++ (with all; [
-    blackfire
-  ]));
-in {
-  # Enable the probe extension for PHP-FPM.
-  services.phpfpm = {
-    phpPackage = php;
-  };
-
-  # Enable and configure the agent.
-  services.blackfire-agent = {
-    enable = true;
-    settings = {
-      # You will need to get credentials at https://blackfire.io/my/settings/credentials
-      # You can also use other options described in https://blackfire.io/docs/up-and-running/configuration/agent
-      server-id = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
-      server-token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
-    };
-  };
-
-  # Make the agent run on start-up.
-  # (WantedBy= from the upstream unit not respected: https://github.com/NixOS/nixpkgs/issues/81138)
-  # Alternately, you can start it manually with `systemctl start blackfire-agent`.
-  systemd.services.blackfire-agent.wantedBy = [ "phpfpm-foo.service" ];
-}</programlisting>
- </para>
- <para>
-  On your developer machine, you will also want to install <link xlink:href="https://blackfire.io/docs/up-and-running/installation#install-a-profiling-client">the client</link> (see <package>blackfire</package> package) or the browser extension to actually trigger the profiling.
- </para>
-</chapter>
diff --git a/nixos/modules/services/development/gemstash.nix b/nixos/modules/services/development/gemstash.nix
new file mode 100644
index 00000000000..eb7ccb98bde
--- /dev/null
+++ b/nixos/modules/services/development/gemstash.nix
@@ -0,0 +1,103 @@
+{ lib, pkgs, config, ... }:
+with lib;
+
+let
+  settingsFormat = pkgs.formats.yaml { };
+
+  # gemstash uses a yaml config where the keys are ruby symbols,
+  # which means they start with ':'. This would be annoying to use
+  # on the nix side, so we rewrite plain names instead.
+  prefixColon = s: listToAttrs (map
+    (attrName: {
+      name = ":${attrName}";
+      value =
+        if isAttrs s.${attrName}
+        then prefixColon s."${attrName}"
+        else s."${attrName}";
+    })
+    (attrNames s));
+
+  # parse the port number out of the tcp://ip:port bind setting string
+  parseBindPort = bind: strings.toInt (last (strings.splitString ":" bind));
+
+  cfg = config.services.gemstash;
+in
+{
+  options.services.gemstash = {
+    enable = mkEnableOption (lib.mdDoc "gemstash service");
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Whether to open the firewall for the port in {option}`services.gemstash.bind`.
+      '';
+    };
+
+    settings = mkOption {
+      default = {};
+      description = lib.mdDoc ''
+        Configuration for Gemstash. The details can be found at in
+        [gemstash documentation](https://github.com/rubygems/gemstash/blob/master/man/gemstash-configuration.5.md).
+        Each key set here is automatically prefixed with ":" to match the gemstash expectations.
+      '';
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+        options = {
+          base_path = mkOption {
+            type = types.path;
+            default = "/var/lib/gemstash";
+            description = lib.mdDoc "Path to store the gem files and the sqlite database. If left unchanged, the directory will be created.";
+          };
+          bind = mkOption {
+            type = types.str;
+            default = "tcp://0.0.0.0:9292";
+            description = lib.mdDoc "Host and port combination for the server to listen on.";
+          };
+          db_adapter = mkOption {
+            type = types.nullOr (types.enum [ "sqlite3" "postgres" "mysql" "mysql2" ]);
+            default = null;
+            description = lib.mdDoc "Which database type to use. For choices other than sqlite3, the dbUrl has to be specified as well.";
+          };
+          db_url = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = lib.mdDoc "The database to connect to when using postgres, mysql, or mysql2.";
+          };
+        };
+      };
+    };
+  };
+
+  config =
+    mkIf cfg.enable {
+      users = {
+        users.gemstash = {
+          group = "gemstash";
+          isSystemUser = true;
+        };
+        groups.gemstash = { };
+      };
+
+      networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ (parseBindPort cfg.settings.bind) ];
+
+      systemd.services.gemstash = {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = mkMerge [
+          {
+            ExecStart = "${pkgs.gemstash}/bin/gemstash start --no-daemonize --config-file ${settingsFormat.generate "gemstash.yaml" (prefixColon cfg.settings)}";
+            NoNewPrivileges = true;
+            User = "gemstash";
+            Group = "gemstash";
+            PrivateTmp = true;
+            RestrictSUIDSGID = true;
+            LockPersonality = true;
+          }
+          (mkIf (cfg.settings.base_path == "/var/lib/gemstash") {
+            StateDirectory = "gemstash";
+          })
+        ];
+      };
+    };
+}
diff --git a/nixos/modules/services/development/lorri.nix b/nixos/modules/services/development/lorri.nix
index 8c64e3d9a56..74f56f5890f 100644
--- a/nixos/modules/services/development/lorri.nix
+++ b/nixos/modules/services/development/lorri.nix
@@ -50,6 +50,6 @@ in {
       };
     };
 
-    environment.systemPackages = [ cfg.package ];
+    environment.systemPackages = [ cfg.package pkgs.direnv ];
   };
 }
diff --git a/nixos/modules/services/development/zammad.nix b/nixos/modules/services/development/zammad.nix
index 7de11b08b7e..7dd143eebf1 100644
--- a/nixos/modules/services/development/zammad.nix
+++ b/nixos/modules/services/development/zammad.nix
@@ -28,7 +28,7 @@ in
 
   options = {
     services.zammad = {
-      enable = mkEnableOption (lib.mdDoc "Zammad, a web-based, open source user support/ticketing solution.");
+      enable = mkEnableOption (lib.mdDoc "Zammad, a web-based, open source user support/ticketing solution");
 
       package = mkOption {
         type = types.package;
@@ -254,7 +254,7 @@ in
       preStart = ''
         # Blindly copy the whole project here.
         chmod -R +w .
-        rm -rf ./public/assets/*
+        rm -rf ./public/assets/
         rm -rf ./tmp/*
         rm -rf ./log/*
         cp -r --no-preserve=owner ${cfg.package}/* .
diff --git a/nixos/modules/services/editors/emacs.md b/nixos/modules/services/editors/emacs.md
new file mode 100644
index 00000000000..72364b29514
--- /dev/null
+++ b/nixos/modules/services/editors/emacs.md
@@ -0,0 +1,420 @@
+# Emacs {#module-services-emacs}
+
+<!--
+    Documentation contributors:
+      Damien Cassou @DamienCassou
+      Thomas Tuegel @ttuegel
+      Rodney Lorrimar @rvl
+      Adam Hoese @adisbladis
+  -->
+
+[Emacs](https://www.gnu.org/software/emacs/) is an
+extensible, customizable, self-documenting real-time display editor — and
+more. At its core is an interpreter for Emacs Lisp, a dialect of the Lisp
+programming language with extensions to support text editing.
+
+Emacs runs within a graphical desktop environment using the X Window System,
+but works equally well on a text terminal. Under
+macOS, a "Mac port" edition is available, which
+uses Apple's native GUI frameworks.
+
+Nixpkgs provides a superior environment for
+running Emacs. It's simple to create custom builds
+by overriding the default packages. Chaotic collections of Emacs Lisp code
+and extensions can be brought under control using declarative package
+management. NixOS even provides a
+{command}`systemd` user service for automatically starting the Emacs
+daemon.
+
+## Installing Emacs {#module-services-emacs-installing}
+
+Emacs can be installed in the normal way for Nix (see
+[](#sec-package-management)). In addition, a NixOS
+*service* can be enabled.
+
+### The Different Releases of Emacs {#module-services-emacs-releases}
+
+Nixpkgs defines several basic Emacs packages.
+The following are attributes belonging to the {var}`pkgs` set:
+
+  {var}`emacs`
+  : The latest stable version of Emacs using the [GTK 2](http://www.gtk.org)
+    widget toolkit.
+
+  {var}`emacs-nox`
+  : Emacs built without any dependency on X11 libraries.
+
+  {var}`emacsMacport`
+  : Emacs with the "Mac port" patches, providing a more native look and
+    feel under macOS.
+
+If those aren't suitable, then the following imitation Emacs editors are
+also available in Nixpkgs:
+[Zile](https://www.gnu.org/software/zile/),
+[mg](http://homepage.boetes.org/software/mg/),
+[Yi](http://yi-editor.github.io/),
+[jmacs](https://joe-editor.sourceforge.io/).
+
+### Adding Packages to Emacs {#module-services-emacs-adding-packages}
+
+Emacs includes an entire ecosystem of functionality beyond text editing,
+including a project planner, mail and news reader, debugger interface,
+calendar, and more.
+
+Most extensions are gotten with the Emacs packaging system
+({file}`package.el`) from
+[Emacs Lisp Package Archive (ELPA)](https://elpa.gnu.org/),
+[MELPA](https://melpa.org/),
+[MELPA Stable](https://stable.melpa.org/), and
+[Org ELPA](http://orgmode.org/elpa.html). Nixpkgs is
+regularly updated to mirror all these archives.
+
+Under NixOS, you can continue to use
+`package-list-packages` and
+`package-install` to install packages. You can also
+declare the set of Emacs packages you need using the derivations from
+Nixpkgs. The rest of this section discusses declarative installation of
+Emacs packages through nixpkgs.
+
+The first step to declare the list of packages you want in your Emacs
+installation is to create a dedicated derivation. This can be done in a
+dedicated {file}`emacs.nix` file such as:
+
+::: {.example #ex-emacsNix}
+### Nix expression to build Emacs with packages (`emacs.nix`)
+
+```nix
+/*
+This is a nix expression to build Emacs and some Emacs packages I like
+from source on any distribution where Nix is installed. This will install
+all the dependencies from the nixpkgs repository and build the binary files
+without interfering with the host distribution.
+
+To build the project, type the following from the current directory:
+
+$ nix-build emacs.nix
+
+To run the newly compiled executable:
+
+$ ./result/bin/emacs
+*/
+
+# The first non-comment line in this file indicates that
+# the whole file represents a function.
+{ pkgs ? import <nixpkgs> {} }:
+
+let
+  # The let expression below defines a myEmacs binding pointing to the
+  # current stable version of Emacs. This binding is here to separate
+  # the choice of the Emacs binary from the specification of the
+  # required packages.
+  myEmacs = pkgs.emacs;
+  # This generates an emacsWithPackages function. It takes a single
+  # argument: a function from a package set to a list of packages
+  # (the packages that will be available in Emacs).
+  emacsWithPackages = (pkgs.emacsPackagesFor myEmacs).emacsWithPackages;
+in
+  # The rest of the file specifies the list of packages to install. In the
+  # example, two packages (magit and zerodark-theme) are taken from
+  # MELPA stable.
+  emacsWithPackages (epkgs: (with epkgs.melpaStablePackages; [
+    magit          # ; Integrate git <C-x g>
+    zerodark-theme # ; Nicolas' theme
+  ])
+  # Two packages (undo-tree and zoom-frm) are taken from MELPA.
+  ++ (with epkgs.melpaPackages; [
+    undo-tree      # ; <C-x u> to show the undo tree
+    zoom-frm       # ; increase/decrease font size for all buffers %lt;C-x C-+>
+  ])
+  # Three packages are taken from GNU ELPA.
+  ++ (with epkgs.elpaPackages; [
+    auctex         # ; LaTeX mode
+    beacon         # ; highlight my cursor when scrolling
+    nameless       # ; hide current package name everywhere in elisp code
+  ])
+  # notmuch is taken from a nixpkgs derivation which contains an Emacs mode.
+  ++ [
+    pkgs.notmuch   # From main packages set
+  ])
+```
+:::
+
+The result of this configuration will be an {command}`emacs`
+command which launches Emacs with all of your chosen packages in the
+{var}`load-path`.
+
+You can check that it works by executing this in a terminal:
+```ShellSession
+$ nix-build emacs.nix
+$ ./result/bin/emacs -q
+```
+and then typing `M-x package-initialize`. Check that you
+can use all the packages you want in this Emacs instance. For example, try
+switching to the zerodark theme through `M-x load-theme <RET> zerodark <RET> y`.
+
+::: {.tip}
+A few popular extensions worth checking out are: auctex, company,
+edit-server, flycheck, helm, iedit, magit, multiple-cursors, projectile,
+and yasnippet.
+:::
+
+The list of available packages in the various ELPA repositories can be seen
+with the following commands:
+::: {.example #module-services-emacs-querying-packages}
+### Querying Emacs packages
+
+```
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.elpaPackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.melpaPackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.melpaStablePackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.orgPackages
+```
+:::
+
+If you are on NixOS, you can install this particular Emacs for all users by
+adding it to the list of system packages (see
+[](#sec-declarative-package-mgmt)). Simply modify your file
+{file}`configuration.nix` to make it contain:
+::: {.example #module-services-emacs-configuration-nix}
+### Custom Emacs in `configuration.nix`
+
+```
+{
+ environment.systemPackages = [
+   # [...]
+   (import /path/to/emacs.nix { inherit pkgs; })
+  ];
+}
+```
+:::
+
+In this case, the next {command}`nixos-rebuild switch` will take
+care of adding your {command}`emacs` to the {var}`PATH`
+environment variable (see [](#sec-changing-config)).
+
+<!-- fixme: i think the following is better done with config.nix
+https://nixos.org/nixpkgs/manual/#sec-modify-via-packageOverrides
+-->
+
+If you are not on NixOS or want to install this particular Emacs only for
+yourself, you can do so by adding it to your
+{file}`~/.config/nixpkgs/config.nix` (see
+[Nixpkgs manual](https://nixos.org/nixpkgs/manual/#sec-modify-via-packageOverrides)):
+::: {.example #module-services-emacs-config-nix}
+### Custom Emacs in `~/.config/nixpkgs/config.nix`
+
+```
+{
+  packageOverrides = super: let self = super.pkgs; in {
+    myemacs = import /path/to/emacs.nix { pkgs = self; };
+  };
+}
+```
+:::
+
+In this case, the next `nix-env -f '<nixpkgs>' -iA
+myemacs` will take care of adding your emacs to the
+{var}`PATH` environment variable.
+
+### Advanced Emacs Configuration {#module-services-emacs-advanced}
+
+If you want, you can tweak the Emacs package itself from your
+{file}`emacs.nix`. For example, if you want to have a
+GTK 3-based Emacs instead of the default GTK 2-based binary and remove the
+automatically generated {file}`emacs.desktop` (useful if you
+only use {command}`emacsclient`), you can change your file
+{file}`emacs.nix` in this way:
+
+::: {.example #ex-emacsGtk3Nix}
+### Custom Emacs build
+
+```
+{ pkgs ? import <nixpkgs> {} }:
+let
+  myEmacs = (pkgs.emacs.override {
+    # Use gtk3 instead of the default gtk2
+    withGTK3 = true;
+    withGTK2 = false;
+  }).overrideAttrs (attrs: {
+    # I don't want emacs.desktop file because I only use
+    # emacsclient.
+    postInstall = (attrs.postInstall or "") + ''
+      rm $out/share/applications/emacs.desktop
+    '';
+  });
+in [...]
+```
+:::
+
+After building this file as shown in [](#ex-emacsNix), you
+will get an GTK 3-based Emacs binary pre-loaded with your favorite packages.
+
+## Running Emacs as a Service {#module-services-emacs-running}
+
+NixOS provides an optional
+{command}`systemd` service which launches
+[Emacs daemon](https://www.gnu.org/software/emacs/manual/html_node/emacs/Emacs-Server.html)
+with the user's login session.
+
+*Source:* {file}`modules/services/editors/emacs.nix`
+
+### Enabling the Service {#module-services-emacs-enabling}
+
+To install and enable the {command}`systemd` user service for Emacs
+daemon, add the following to your {file}`configuration.nix`:
+```
+services.emacs.enable = true;
+services.emacs.package = import /home/cassou/.emacs.d { pkgs = pkgs; };
+```
+
+The {var}`services.emacs.package` option allows a custom
+derivation to be used, for example, one created by
+`emacsWithPackages`.
+
+Ensure that the Emacs server is enabled for your user's Emacs
+configuration, either by customizing the {var}`server-mode`
+variable, or by adding `(server-start)` to
+{file}`~/.emacs.d/init.el`.
+
+To start the daemon, execute the following:
+```ShellSession
+$ nixos-rebuild switch  # to activate the new configuration.nix
+$ systemctl --user daemon-reload        # to force systemd reload
+$ systemctl --user start emacs.service  # to start the Emacs daemon
+```
+The server should now be ready to serve Emacs clients.
+
+### Starting the client {#module-services-emacs-starting-client}
+
+Ensure that the emacs server is enabled, either by customizing the
+{var}`server-mode` variable, or by adding
+`(server-start)` to {file}`~/.emacs`.
+
+To connect to the emacs daemon, run one of the following:
+```
+emacsclient FILENAME
+emacsclient --create-frame  # opens a new frame (window)
+emacsclient --create-frame --tty  # opens a new frame on the current terminal
+```
+
+### Configuring the {var}`EDITOR` variable {#module-services-emacs-editor-variable}
+
+<!--<title>{command}`emacsclient` as the Default Editor</title>-->
+
+If [](#opt-services.emacs.defaultEditor) is
+`true`, the {var}`EDITOR` variable will be set
+to a wrapper script which launches {command}`emacsclient`.
+
+Any setting of {var}`EDITOR` in the shell config files will
+override {var}`services.emacs.defaultEditor`. To make sure
+{var}`EDITOR` refers to the Emacs wrapper script, remove any
+existing {var}`EDITOR` assignment from
+{file}`.profile`, {file}`.bashrc`,
+{file}`.zshenv` or any other shell config file.
+
+If you have formed certain bad habits when editing files, these can be
+corrected with a shell alias to the wrapper script:
+```
+alias vi=$EDITOR
+```
+
+### Per-User Enabling of the Service {#module-services-emacs-per-user}
+
+In general, {command}`systemd` user services are globally enabled
+by symlinks in {file}`/etc/systemd/user`. In the case where
+Emacs daemon is not wanted for all users, it is possible to install the
+service but not globally enable it:
+```
+services.emacs.enable = false;
+services.emacs.install = true;
+```
+
+To enable the {command}`systemd` user service for just the
+currently logged in user, run:
+```
+systemctl --user enable emacs
+```
+This will add the symlink
+{file}`~/.config/systemd/user/emacs.service`.
+
+## Configuring Emacs {#module-services-emacs-configuring}
+
+The Emacs init file should be changed to load the extension packages at
+startup:
+
+::: {.example #module-services-emacs-package-initialisation}
+### Package initialization in `.emacs`
+
+```
+(require 'package)
+
+;; optional. makes unpure packages archives unavailable
+(setq package-archives nil)
+
+(setq package-enable-at-startup nil)
+(package-initialize)
+```
+:::
+
+After the declarative emacs package configuration has been tested,
+previously downloaded packages can be cleaned up by removing
+{file}`~/.emacs.d/elpa` (do make a backup first, in case you
+forgot a package).
+
+<!--
+      todo: is it worth documenting customizations for
+      server-switch-hook, server-done-hook?
+  -->
+
+### A Major Mode for Nix Expressions {#module-services-emacs-major-mode}
+
+Of interest may be {var}`melpaPackages.nix-mode`, which
+provides syntax highlighting for the Nix language. This is particularly
+convenient if you regularly edit Nix files.
+
+### Accessing man pages {#module-services-emacs-man-pages}
+
+You can use `woman` to get completion of all available
+man pages. For example, type `M-x woman <RET> nixos-rebuild <RET>.`
+
+### Editing DocBook 5 XML Documents {#sec-emacs-docbook-xml}
+
+Emacs includes
+[nXML](https://www.gnu.org/software/emacs/manual/html_node/nxml-mode/Introduction.html),
+a major-mode for validating and editing XML documents. When editing DocBook
+5.0 documents, such as [this one](#book-nixos-manual),
+nXML needs to be configured with the relevant schema, which is not
+included.
+
+To install the DocBook 5.0 schemas, either add
+{var}`pkgs.docbook5` to [](#opt-environment.systemPackages)
+([NixOS](#sec-declarative-package-mgmt)), or run
+`nix-env -f '<nixpkgs>' -iA docbook5`
+([Nix](#sec-ad-hoc-packages)).
+
+Then customize the variable {var}`rng-schema-locating-files` to
+include {file}`~/.emacs.d/schemas.xml` and put the following
+text into that file:
+::: {.example #ex-emacs-docbook-xml}
+### nXML Schema Configuration (`~/.emacs.d/schemas.xml`)
+
+```xml
+<?xml version="1.0"?>
+<!--
+  To let emacs find this file, evaluate:
+  (add-to-list 'rng-schema-locating-files "~/.emacs.d/schemas.xml")
+-->
+<locatingRules xmlns="http://thaiopensource.com/ns/locating-rules/1.0">
+  <!--
+    Use this variation if pkgs.docbook5 is added to environment.systemPackages
+  -->
+  <namespace ns="http://docbook.org/ns/docbook"
+             uri="/run/current-system/sw/share/xml/docbook-5.0/rng/docbookxi.rnc"/>
+  <!--
+    Use this variation if installing schema with "nix-env -iA pkgs.docbook5".
+  <namespace ns="http://docbook.org/ns/docbook"
+             uri="../.nix-profile/share/xml/docbook-5.0/rng/docbookxi.rnc"/>
+  -->
+</locatingRules>
+```
+:::
diff --git a/nixos/modules/services/editors/emacs.nix b/nixos/modules/services/editors/emacs.nix
index 5ae28cd9bbb..2be46e47d64 100644
--- a/nixos/modules/services/editors/emacs.nix
+++ b/nixos/modules/services/editors/emacs.nix
@@ -99,5 +99,5 @@ in
     environment.variables.EDITOR = mkIf cfg.defaultEditor (mkOverride 900 "${editorScript}/bin/emacseditor");
   };
 
-  meta.doc = ./emacs.xml;
+  meta.doc = ./emacs.md;
 }
diff --git a/nixos/modules/services/editors/emacs.xml b/nixos/modules/services/editors/emacs.xml
deleted file mode 100644
index fd99ee9442c..00000000000
--- a/nixos/modules/services/editors/emacs.xml
+++ /dev/null
@@ -1,580 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-emacs">
- <title>Emacs</title>
-<!--
-    Documentation contributors:
-      Damien Cassou @DamienCassou
-      Thomas Tuegel @ttuegel
-      Rodney Lorrimar @rvl
-      Adam Hoese @adisbladis
-  -->
- <para>
-  <link xlink:href="https://www.gnu.org/software/emacs/">Emacs</link> is an
-  extensible, customizable, self-documenting real-time display editor — and
-  more. At its core is an interpreter for Emacs Lisp, a dialect of the Lisp
-  programming language with extensions to support text editing.
- </para>
- <para>
-  Emacs runs within a graphical desktop environment using the X Window System,
-  but works equally well on a text terminal. Under
-  <productname>macOS</productname>, a "Mac port" edition is available, which
-  uses Apple's native GUI frameworks.
- </para>
- <para>
-  <productname>Nixpkgs</productname> provides a superior environment for
-  running <application>Emacs</application>. It's simple to create custom builds
-  by overriding the default packages. Chaotic collections of Emacs Lisp code
-  and extensions can be brought under control using declarative package
-  management. <productname>NixOS</productname> even provides a
-  <command>systemd</command> user service for automatically starting the Emacs
-  daemon.
- </para>
- <section xml:id="module-services-emacs-installing">
-  <title>Installing <application>Emacs</application></title>
-
-  <para>
-   Emacs can be installed in the normal way for Nix (see
-   <xref linkend="sec-package-management" />). In addition, a NixOS
-   <emphasis>service</emphasis> can be enabled.
-  </para>
-
-  <section xml:id="module-services-emacs-releases">
-   <title>The Different Releases of Emacs</title>
-
-   <para>
-    <productname>Nixpkgs</productname> defines several basic Emacs packages.
-    The following are attributes belonging to the <varname>pkgs</varname> set:
-    <variablelist>
-     <varlistentry>
-      <term>
-       <varname>emacs</varname>
-      </term>
-      <term>
-       <varname>emacs</varname>
-      </term>
-      <listitem>
-       <para>
-        The latest stable version of Emacs using the
-        <link
-                xlink:href="http://www.gtk.org">GTK 2</link>
-        widget toolkit.
-       </para>
-      </listitem>
-     </varlistentry>
-     <varlistentry>
-      <term>
-       <varname>emacs-nox</varname>
-      </term>
-      <listitem>
-       <para>
-        Emacs built without any dependency on X11 libraries.
-       </para>
-      </listitem>
-     </varlistentry>
-     <varlistentry>
-      <term>
-       <varname>emacsMacport</varname>
-      </term>
-      <term>
-       <varname>emacsMacport</varname>
-      </term>
-      <listitem>
-       <para>
-        Emacs with the "Mac port" patches, providing a more native look and
-        feel under macOS.
-       </para>
-      </listitem>
-     </varlistentry>
-    </variablelist>
-   </para>
-
-   <para>
-    If those aren't suitable, then the following imitation Emacs editors are
-    also available in Nixpkgs:
-    <link xlink:href="https://www.gnu.org/software/zile/">Zile</link>,
-    <link xlink:href="http://homepage.boetes.org/software/mg/">mg</link>,
-    <link xlink:href="http://yi-editor.github.io/">Yi</link>,
-    <link xlink:href="https://joe-editor.sourceforge.io/">jmacs</link>.
-   </para>
-  </section>
-
-  <section xml:id="module-services-emacs-adding-packages">
-   <title>Adding Packages to Emacs</title>
-
-   <para>
-    Emacs includes an entire ecosystem of functionality beyond text editing,
-    including a project planner, mail and news reader, debugger interface,
-    calendar, and more.
-   </para>
-
-   <para>
-    Most extensions are gotten with the Emacs packaging system
-    (<filename>package.el</filename>) from
-    <link
-        xlink:href="https://elpa.gnu.org/">Emacs Lisp Package Archive
-    (<acronym>ELPA</acronym>)</link>,
-    <link xlink:href="https://melpa.org/"><acronym>MELPA</acronym></link>,
-    <link xlink:href="https://stable.melpa.org/">MELPA Stable</link>, and
-    <link xlink:href="http://orgmode.org/elpa.html">Org ELPA</link>. Nixpkgs is
-    regularly updated to mirror all these archives.
-   </para>
-
-   <para>
-    Under NixOS, you can continue to use
-    <function>package-list-packages</function> and
-    <function>package-install</function> to install packages. You can also
-    declare the set of Emacs packages you need using the derivations from
-    Nixpkgs. The rest of this section discusses declarative installation of
-    Emacs packages through nixpkgs.
-   </para>
-
-   <para>
-    The first step to declare the list of packages you want in your Emacs
-    installation is to create a dedicated derivation. This can be done in a
-    dedicated <filename>emacs.nix</filename> file such as:
-    <example xml:id="ex-emacsNix">
-     <title>Nix expression to build Emacs with packages (<filename>emacs.nix</filename>)</title>
-<programlisting language="nix">
-/*
-This is a nix expression to build Emacs and some Emacs packages I like
-from source on any distribution where Nix is installed. This will install
-all the dependencies from the nixpkgs repository and build the binary files
-without interfering with the host distribution.
-
-To build the project, type the following from the current directory:
-
-$ nix-build emacs.nix
-
-To run the newly compiled executable:
-
-$ ./result/bin/emacs
-*/
-{ pkgs ? import &lt;nixpkgs&gt; {} }: <co xml:id="ex-emacsNix-1" />
-
-let
-  myEmacs = pkgs.emacs; <co xml:id="ex-emacsNix-2" />
-  emacsWithPackages = (pkgs.emacsPackagesFor myEmacs).emacsWithPackages; <co xml:id="ex-emacsNix-3" />
-in
-  emacsWithPackages (epkgs: (with epkgs.melpaStablePackages; [ <co xml:id="ex-emacsNix-4" />
-    magit          # ; Integrate git &lt;C-x g&gt;
-    zerodark-theme # ; Nicolas' theme
-  ]) ++ (with epkgs.melpaPackages; [ <co xml:id="ex-emacsNix-5" />
-    undo-tree      # ; &lt;C-x u&gt; to show the undo tree
-    zoom-frm       # ; increase/decrease font size for all buffers %lt;C-x C-+&gt;
-  ]) ++ (with epkgs.elpaPackages; [ <co xml:id="ex-emacsNix-6" />
-    auctex         # ; LaTeX mode
-    beacon         # ; highlight my cursor when scrolling
-    nameless       # ; hide current package name everywhere in elisp code
-  ]) ++ [
-    pkgs.notmuch   # From main packages set <co xml:id="ex-emacsNix-7" />
-  ])
-</programlisting>
-    </example>
-    <calloutlist>
-     <callout arearefs="ex-emacsNix-1">
-      <para>
-       The first non-comment line in this file (<literal>{ pkgs ? ...
-       }</literal>) indicates that the whole file represents a function.
-      </para>
-     </callout>
-     <callout arearefs="ex-emacsNix-2">
-      <para>
-       The <varname>let</varname> expression below defines a
-       <varname>myEmacs</varname> binding pointing to the current stable
-       version of Emacs. This binding is here to separate the choice of the
-       Emacs binary from the specification of the required packages.
-      </para>
-     </callout>
-     <callout arearefs="ex-emacsNix-3">
-      <para>
-       This generates an <varname>emacsWithPackages</varname> function. It
-       takes a single argument: a function from a package set to a list of
-       packages (the packages that will be available in Emacs).
-      </para>
-     </callout>
-     <callout arearefs="ex-emacsNix-4">
-      <para>
-       The rest of the file specifies the list of packages to install. In the
-       example, two packages (<varname>magit</varname> and
-       <varname>zerodark-theme</varname>) are taken from MELPA stable.
-      </para>
-     </callout>
-     <callout arearefs="ex-emacsNix-5">
-      <para>
-       Two packages (<varname>undo-tree</varname> and
-       <varname>zoom-frm</varname>) are taken from MELPA.
-      </para>
-     </callout>
-     <callout arearefs="ex-emacsNix-6">
-      <para>
-       Three packages are taken from GNU ELPA.
-      </para>
-     </callout>
-     <callout arearefs="ex-emacsNix-7">
-      <para>
-       <varname>notmuch</varname> is taken from a nixpkgs derivation which
-       contains an Emacs mode.
-      </para>
-     </callout>
-    </calloutlist>
-   </para>
-
-   <para>
-    The result of this configuration will be an <command>emacs</command>
-    command which launches Emacs with all of your chosen packages in the
-    <varname>load-path</varname>.
-   </para>
-
-   <para>
-    You can check that it works by executing this in a terminal:
-<screen>
-<prompt>$ </prompt>nix-build emacs.nix
-<prompt>$ </prompt>./result/bin/emacs -q
-</screen>
-    and then typing <literal>M-x package-initialize</literal>. Check that you
-    can use all the packages you want in this Emacs instance. For example, try
-    switching to the zerodark theme through <literal>M-x load-theme &lt;RET&gt;
-    zerodark &lt;RET&gt; y</literal>.
-   </para>
-
-   <tip>
-    <para>
-     A few popular extensions worth checking out are: auctex, company,
-     edit-server, flycheck, helm, iedit, magit, multiple-cursors, projectile,
-     and yasnippet.
-    </para>
-   </tip>
-
-   <para>
-    The list of available packages in the various ELPA repositories can be seen
-    with the following commands:
-    <example xml:id="module-services-emacs-querying-packages">
-     <title>Querying Emacs packages</title>
-<programlisting><![CDATA[
-nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.elpaPackages
-nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.melpaPackages
-nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.melpaStablePackages
-nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.orgPackages
-]]></programlisting>
-    </example>
-   </para>
-
-   <para>
-    If you are on NixOS, you can install this particular Emacs for all users by
-    adding it to the list of system packages (see
-    <xref linkend="sec-declarative-package-mgmt" />). Simply modify your file
-    <filename>configuration.nix</filename> to make it contain:
-    <example xml:id="module-services-emacs-configuration-nix">
-     <title>Custom Emacs in <filename>configuration.nix</filename></title>
-<programlisting><![CDATA[
-{
- environment.systemPackages = [
-   # [...]
-   (import /path/to/emacs.nix { inherit pkgs; })
-  ];
-}
-]]></programlisting>
-    </example>
-   </para>
-
-   <para>
-    In this case, the next <command>nixos-rebuild switch</command> will take
-    care of adding your <command>emacs</command> to the <varname>PATH</varname>
-    environment variable (see <xref linkend="sec-changing-config" />).
-   </para>
-
-<!-- fixme: i think the following is better done with config.nix
-https://nixos.org/nixpkgs/manual/#sec-modify-via-packageOverrides
--->
-
-   <para>
-    If you are not on NixOS or want to install this particular Emacs only for
-    yourself, you can do so by adding it to your
-    <filename>~/.config/nixpkgs/config.nix</filename> (see
-    <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-modify-via-packageOverrides">Nixpkgs
-    manual</link>):
-    <example xml:id="module-services-emacs-config-nix">
-     <title>Custom Emacs in <filename>~/.config/nixpkgs/config.nix</filename></title>
-<programlisting><![CDATA[
-{
-  packageOverrides = super: let self = super.pkgs; in {
-    myemacs = import /path/to/emacs.nix { pkgs = self; };
-  };
-}
-]]></programlisting>
-    </example>
-   </para>
-
-   <para>
-    In this case, the next <literal>nix-env -f '&lt;nixpkgs&gt;' -iA
-    myemacs</literal> will take care of adding your emacs to the
-    <varname>PATH</varname> environment variable.
-   </para>
-  </section>
-
-  <section xml:id="module-services-emacs-advanced">
-   <title>Advanced Emacs Configuration</title>
-
-   <para>
-    If you want, you can tweak the Emacs package itself from your
-    <filename>emacs.nix</filename>. For example, if you want to have a
-    GTK 3-based Emacs instead of the default GTK 2-based binary and remove the
-    automatically generated <filename>emacs.desktop</filename> (useful if you
-    only use <command>emacsclient</command>), you can change your file
-    <filename>emacs.nix</filename> in this way:
-   </para>
-
-   <example xml:id="ex-emacsGtk3Nix">
-    <title>Custom Emacs build</title>
-<programlisting><![CDATA[
-{ pkgs ? import <nixpkgs> {} }:
-let
-  myEmacs = (pkgs.emacs.override {
-    # Use gtk3 instead of the default gtk2
-    withGTK3 = true;
-    withGTK2 = false;
-  }).overrideAttrs (attrs: {
-    # I don't want emacs.desktop file because I only use
-    # emacsclient.
-    postInstall = (attrs.postInstall or "") + ''
-      rm $out/share/applications/emacs.desktop
-    '';
-  });
-in [...]
-]]></programlisting>
-   </example>
-
-   <para>
-    After building this file as shown in <xref linkend="ex-emacsNix" />, you
-    will get an GTK 3-based Emacs binary pre-loaded with your favorite packages.
-   </para>
-  </section>
- </section>
- <section xml:id="module-services-emacs-running">
-  <title>Running Emacs as a Service</title>
-
-  <para>
-   <productname>NixOS</productname> provides an optional
-   <command>systemd</command> service which launches
-   <link xlink:href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Emacs-Server.html">
-   Emacs daemon </link> with the user's login session.
-  </para>
-
-  <para>
-   <emphasis>Source:</emphasis>
-   <filename>modules/services/editors/emacs.nix</filename>
-  </para>
-
-  <section xml:id="module-services-emacs-enabling">
-   <title>Enabling the Service</title>
-
-   <para>
-    To install and enable the <command>systemd</command> user service for Emacs
-    daemon, add the following to your <filename>configuration.nix</filename>:
-<programlisting>
-<xref linkend="opt-services.emacs.enable"/> = true;
-<xref linkend="opt-services.emacs.package"/> = import /home/cassou/.emacs.d { pkgs = pkgs; };
-</programlisting>
-   </para>
-
-   <para>
-    The <varname>services.emacs.package</varname> option allows a custom
-    derivation to be used, for example, one created by
-    <function>emacsWithPackages</function>.
-   </para>
-
-   <para>
-    Ensure that the Emacs server is enabled for your user's Emacs
-    configuration, either by customizing the <varname>server-mode</varname>
-    variable, or by adding <literal>(server-start)</literal> to
-    <filename>~/.emacs.d/init.el</filename>.
-   </para>
-
-   <para>
-    To start the daemon, execute the following:
-<screen>
-<prompt>$ </prompt>nixos-rebuild switch  # to activate the new configuration.nix
-<prompt>$ </prompt>systemctl --user daemon-reload        # to force systemd reload
-<prompt>$ </prompt>systemctl --user start emacs.service  # to start the Emacs daemon
-</screen>
-    The server should now be ready to serve Emacs clients.
-   </para>
-  </section>
-
-  <section xml:id="module-services-emacs-starting-client">
-   <title>Starting the client</title>
-
-   <para>
-    Ensure that the emacs server is enabled, either by customizing the
-    <varname>server-mode</varname> variable, or by adding
-    <literal>(server-start)</literal> to <filename>~/.emacs</filename>.
-   </para>
-
-   <para>
-    To connect to the emacs daemon, run one of the following:
-<programlisting><![CDATA[
-emacsclient FILENAME
-emacsclient --create-frame  # opens a new frame (window)
-emacsclient --create-frame --tty  # opens a new frame on the current terminal
-]]></programlisting>
-   </para>
-  </section>
-
-  <section xml:id="module-services-emacs-editor-variable">
-   <title>Configuring the <varname>EDITOR</varname> variable</title>
-
-<!--<title><command>emacsclient</command> as the Default Editor</title>-->
-
-   <para>
-    If <xref linkend="opt-services.emacs.defaultEditor"/> is
-    <literal>true</literal>, the <varname>EDITOR</varname> variable will be set
-    to a wrapper script which launches <command>emacsclient</command>.
-   </para>
-
-   <para>
-    Any setting of <varname>EDITOR</varname> in the shell config files will
-    override <varname>services.emacs.defaultEditor</varname>. To make sure
-    <varname>EDITOR</varname> refers to the Emacs wrapper script, remove any
-    existing <varname>EDITOR</varname> assignment from
-    <filename>.profile</filename>, <filename>.bashrc</filename>,
-    <filename>.zshenv</filename> or any other shell config file.
-   </para>
-
-   <para>
-    If you have formed certain bad habits when editing files, these can be
-    corrected with a shell alias to the wrapper script:
-<programlisting>alias vi=$EDITOR</programlisting>
-   </para>
-  </section>
-
-  <section xml:id="module-services-emacs-per-user">
-   <title>Per-User Enabling of the Service</title>
-
-   <para>
-    In general, <command>systemd</command> user services are globally enabled
-    by symlinks in <filename>/etc/systemd/user</filename>. In the case where
-    Emacs daemon is not wanted for all users, it is possible to install the
-    service but not globally enable it:
-<programlisting>
-<xref linkend="opt-services.emacs.enable"/> = false;
-<xref linkend="opt-services.emacs.install"/> = true;
-</programlisting>
-   </para>
-
-   <para>
-    To enable the <command>systemd</command> user service for just the
-    currently logged in user, run:
-<programlisting>systemctl --user enable emacs</programlisting>
-    This will add the symlink
-    <filename>~/.config/systemd/user/emacs.service</filename>.
-   </para>
-  </section>
- </section>
- <section xml:id="module-services-emacs-configuring">
-  <title>Configuring Emacs</title>
-
-  <para>
-   The Emacs init file should be changed to load the extension packages at
-   startup:
-   <example xml:id="module-services-emacs-package-initialisation">
-    <title>Package initialization in <filename>.emacs</filename></title>
-<programlisting><![CDATA[
-(require 'package)
-
-;; optional. makes unpure packages archives unavailable
-(setq package-archives nil)
-
-(setq package-enable-at-startup nil)
-(package-initialize)
-]]></programlisting>
-   </example>
-  </para>
-
-  <para>
-   After the declarative emacs package configuration has been tested,
-   previously downloaded packages can be cleaned up by removing
-   <filename>~/.emacs.d/elpa</filename> (do make a backup first, in case you
-   forgot a package).
-  </para>
-
-<!--
-      todo: is it worth documenting customizations for
-      server-switch-hook, server-done-hook?
-  -->
-
-  <section xml:id="module-services-emacs-major-mode">
-   <title>A Major Mode for Nix Expressions</title>
-
-   <para>
-    Of interest may be <varname>melpaPackages.nix-mode</varname>, which
-    provides syntax highlighting for the Nix language. This is particularly
-    convenient if you regularly edit Nix files.
-   </para>
-  </section>
-
-  <section xml:id="module-services-emacs-man-pages">
-   <title>Accessing man pages</title>
-
-   <para>
-    You can use <function>woman</function> to get completion of all available
-    man pages. For example, type <literal>M-x woman &lt;RET&gt; nixos-rebuild
-    &lt;RET&gt;.</literal>
-   </para>
-  </section>
-
-  <section xml:id="sec-emacs-docbook-xml">
-   <title>Editing DocBook 5 XML Documents</title>
-
-   <para>
-    Emacs includes
-    <link
-      xlink:href="https://www.gnu.org/software/emacs/manual/html_node/nxml-mode/Introduction.html">nXML</link>,
-    a major-mode for validating and editing XML documents. When editing DocBook
-    5.0 documents, such as <link linkend="book-nixos-manual">this one</link>,
-    nXML needs to be configured with the relevant schema, which is not
-    included.
-   </para>
-
-   <para>
-    To install the DocBook 5.0 schemas, either add
-    <varname>pkgs.docbook5</varname> to
-    <xref linkend="opt-environment.systemPackages"/>
-    (<link
-      linkend="sec-declarative-package-mgmt">NixOS</link>), or run
-    <literal>nix-env -f '&lt;nixpkgs&gt;' -iA docbook5</literal>
-    (<link linkend="sec-ad-hoc-packages">Nix</link>).
-   </para>
-
-   <para>
-    Then customize the variable <varname>rng-schema-locating-files</varname> to
-    include <filename>~/.emacs.d/schemas.xml</filename> and put the following
-    text into that file:
-    <example xml:id="ex-emacs-docbook-xml">
-     <title>nXML Schema Configuration (<filename>~/.emacs.d/schemas.xml</filename>)</title>
-<programlisting language="xml"><![CDATA[
-<?xml version="1.0"?>
-<!--
-  To let emacs find this file, evaluate:
-  (add-to-list 'rng-schema-locating-files "~/.emacs.d/schemas.xml")
--->
-<locatingRules xmlns="http://thaiopensource.com/ns/locating-rules/1.0">
-  <!--
-    Use this variation if pkgs.docbook5 is added to environment.systemPackages
-  -->
-  <namespace ns="http://docbook.org/ns/docbook"
-             uri="/run/current-system/sw/share/xml/docbook-5.0/rng/docbookxi.rnc"/>
-  <!--
-    Use this variation if installing schema with "nix-env -iA pkgs.docbook5".
-  <namespace ns="http://docbook.org/ns/docbook"
-             uri="../.nix-profile/share/xml/docbook-5.0/rng/docbookxi.rnc"/>
-  -->
-</locatingRules>
-]]></programlisting>
-    </example>
-   </para>
-  </section>
- </section>
-</chapter>
diff --git a/nixos/modules/services/games/asf.nix b/nixos/modules/services/games/asf.nix
index 7585d56b2d7..f15d7077d96 100644
--- a/nixos/modules/services/games/asf.nix
+++ b/nixos/modules/services/games/asf.nix
@@ -245,7 +245,7 @@ in
 
             rm -f www
             ${optionalString cfg.web-ui.enable ''
-              ln -s ${cfg.web-ui.package}/lib/dist www
+              ln -s ${cfg.web-ui.package}/ www
             ''}
           '';
       };
diff --git a/nixos/modules/services/games/factorio.nix b/nixos/modules/services/games/factorio.nix
index 9b15cac149d..b349ffa2375 100644
--- a/nixos/modules/services/games/factorio.nix
+++ b/nixos/modules/services/games/factorio.nix
@@ -294,6 +294,6 @@ in
       };
     };
 
-    networking.firewall.allowedUDPPorts = if cfg.openFirewall then [ cfg.port ] else [];
+    networking.firewall.allowedUDPPorts = optional cfg.openFirewall cfg.port;
   };
 }
diff --git a/nixos/modules/services/games/freeciv.nix b/nixos/modules/services/games/freeciv.nix
index 8b340bb161a..bba27ae4cb5 100644
--- a/nixos/modules/services/games/freeciv.nix
+++ b/nixos/modules/services/games/freeciv.nix
@@ -16,7 +16,7 @@ let
     generate = name: value:
       let mkParam = k: v:
             if v == null then []
-            else if isBool v then if v then [("--"+k)] else []
+            else if isBool v then optional v ("--"+k)
             else [("--"+k) v];
           mkParams = k: v: map (mkParam k) (if isList v then v else [v]);
       in escapeShellArgs (concatLists (concatLists (mapAttrsToList mkParams value)));
@@ -54,7 +54,7 @@ in
             default = 0;
             description = lib.mdDoc "Set debug log level.";
           };
-          options.exit-on-end = mkEnableOption (lib.mdDoc "exit instead of restarting when a game ends.");
+          options.exit-on-end = mkEnableOption (lib.mdDoc "exit instead of restarting when a game ends");
           options.Guests = mkEnableOption (lib.mdDoc "guests to login if auth is enabled");
           options.Newusers = mkEnableOption (lib.mdDoc "new users to login if auth is enabled");
           options.port = mkOption {
diff --git a/nixos/modules/services/games/mchprs.nix b/nixos/modules/services/games/mchprs.nix
new file mode 100644
index 00000000000..a65001b0b3e
--- /dev/null
+++ b/nixos/modules/services/games/mchprs.nix
@@ -0,0 +1,341 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mchprs;
+  settingsFormat = pkgs.formats.toml { };
+
+  whitelistFile = pkgs.writeText "whitelist.json"
+    (builtins.toJSON
+      (mapAttrsToList (n: v: { name = n; uuid = v; }) cfg.whitelist.list));
+
+  configToml =
+    (removeAttrs cfg.settings [ "address" "port" ]) //
+    {
+      bind_address = cfg.settings.address + ":" + toString cfg.settings.port;
+      whitelist = cfg.whitelist.enable;
+    };
+
+  configTomlFile = settingsFormat.generate "Config.toml" configToml;
+in
+{
+  options = {
+    services.mchprs = {
+      enable = mkEnableOption "MCHPRS";
+
+      declarativeSettings = mkOption {
+        type = types.bool;
+        default = false;
+        description = mdDoc ''
+          Whether to use a declarative configuration for MCHPRS.
+        '';
+      };
+
+      declarativeWhitelist = mkOption {
+        type = types.bool;
+        default = false;
+        description = mdDoc ''
+          Whether to use a declarative whitelist.
+          The options {option}`services.mchprs.whitelist.list`
+          will be applied if and only if set to `true`.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/mchprs";
+        description = mdDoc ''
+          Directory to store MCHPRS database and other state/data files.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = mdDoc ''
+          Whether to open ports in the firewall for the server.
+          Only has effect when
+          {option}`services.mchprs.declarativeSettings` is `true`.
+        '';
+      };
+
+      maxRuntime = mkOption {
+        type = types.str;
+        default = "infinity";
+        example = "7d";
+        description = mdDoc ''
+          Automatically restart the server after
+          {option}`services.mchprs.maxRuntime`.
+          The time span format is described here:
+          https://www.freedesktop.org/software/systemd/man/systemd.time.html#Parsing%20Time%20Spans.
+          If `null`, then the server is not restarted automatically.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.mchprs;
+        defaultText = literalExpression "pkgs.mchprs";
+        description = mdDoc "Version of MCHPRS to run.";
+      };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = settingsFormat.type;
+
+          options = {
+            port = mkOption {
+              type = types.port;
+              default = 25565;
+              description = mdDoc ''
+                Port for the server.
+                Only has effect when
+                {option}`services.mchprs.declarativeSettings` is `true`.
+              '';
+            };
+
+            address = mkOption {
+              type = types.str;
+              default = "0.0.0.0";
+              description = mdDoc ''
+                Address for the server.
+                Please use enclosing square brackets when using ipv6.
+                Only has effect when
+                {option}`services.mchprs.declarativeSettings` is `true`.
+              '';
+            };
+
+            motd = mkOption {
+              type = types.str;
+              default = "Minecraft High Performance Redstone Server";
+              description = mdDoc ''
+                Message of the day.
+                Only has effect when
+                {option}`services.mchprs.declarativeSettings` is `true`.
+              '';
+            };
+
+            chat_format = mkOption {
+              type = types.str;
+              default = "<{username}> {message}";
+              description = mdDoc ''
+                How to format chat message interpolating `username`
+                and `message` with curly braces.
+                Only has effect when
+                {option}`services.mchprs.declarativeSettings` is `true`.
+              '';
+            };
+
+            max_players = mkOption {
+              type = types.ints.positive;
+              default = 99999;
+              description = mdDoc ''
+                Maximum number of simultaneous players.
+                Only has effect when
+                {option}`services.mchprs.declarativeSettings` is `true`.
+              '';
+            };
+
+            view_distance = mkOption {
+              type = types.ints.positive;
+              default = 8;
+              description = mdDoc ''
+                Maximal distance (in chunks) between players and loaded chunks.
+                Only has effect when
+                {option}`services.mchprs.declarativeSettings` is `true`.
+              '';
+            };
+
+            bungeecord = mkOption {
+              type = types.bool;
+              default = false;
+              description = mdDoc ''
+                Enable compatibility with
+                [BungeeCord](https://github.com/SpigotMC/BungeeCord).
+                Only has effect when
+                {option}`services.mchprs.declarativeSettings` is `true`.
+              '';
+            };
+
+            schemati = mkOption {
+              type = types.bool;
+              default = false;
+              description = mdDoc ''
+                Mimic the verification and directory layout used by the
+                Open Redstone Engineers
+                [Schemati plugin](https://github.com/OpenRedstoneEngineers/Schemati).
+                Only has effect when
+                {option}`services.mchprs.declarativeSettings` is `true`.
+              '';
+            };
+
+            block_in_hitbox = mkOption {
+              type = types.bool;
+              default = true;
+              description = mdDoc ''
+                Allow placing blocks inside of players
+                (hitbox logic is simplified).
+                Only has effect when
+                {option}`services.mchprs.declarativeSettings` is `true`.
+              '';
+            };
+
+            auto_redpiler = mkOption {
+              type = types.bool;
+              default = true;
+              description = mdDoc ''
+                Use redpiler automatically.
+                Only has effect when
+                {option}`services.mchprs.declarativeSettings` is `true`.
+              '';
+            };
+          };
+        };
+        default = { };
+
+        description = mdDoc ''
+          Configuration for MCHPRS via `Config.toml`.
+          See https://github.com/MCHPR/MCHPRS/blob/master/README.md for documentation.
+        '';
+      };
+
+      whitelist = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = mdDoc ''
+            Whether or not the whitelist (in `whitelist.json`) shoud be enabled.
+            Only has effect when {option}`services.mchprs.declarativeSettings` is `true`.
+          '';
+        };
+
+        list = mkOption {
+          type =
+            let
+              minecraftUUID = types.strMatching
+                "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" // {
+                description = "Minecraft UUID";
+              };
+            in
+            types.attrsOf minecraftUUID;
+          default = { };
+          example = literalExpression ''
+            {
+              username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+              username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
+            };
+          '';
+          description = mdDoc ''
+            Whitelisted players, only has an effect when
+            {option}`services.mchprs.declarativeWhitelist` is
+            `true` and the whitelist is enabled
+            via {option}`services.mchprs.whitelist.enable`.
+            This is a mapping from Minecraft usernames to UUIDs.
+            You can use <https://mcuuid.net/> to get a
+            Minecraft UUID for a username.
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.mchprs = {
+      description = "MCHPRS service user";
+      home = cfg.dataDir;
+      createHome = true;
+      isSystemUser = true;
+      group = "mchprs";
+    };
+    users.groups.mchprs = { };
+
+    systemd.services.mchprs = {
+      description = "MCHPRS Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        ExecStart = "${lib.getExe cfg.package}";
+        Restart = "always";
+        RuntimeMaxSec = cfg.maxRuntime;
+        User = "mchprs";
+        WorkingDirectory = cfg.dataDir;
+
+        StandardOutput = "journal";
+        StandardError = "journal";
+
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DeviceAllow = [ "" ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        UMask = "0077";
+      };
+
+      preStart =
+        (if cfg.declarativeSettings then ''
+          if [ -e .declarativeSettings ]; then
+
+            # Settings were declarative before, no need to back up anything
+            cp -f ${configTomlFile} Config.toml
+
+          else
+
+            # Declarative settings for the first time, backup stateful files
+            cp -b --suffix=.stateful ${configTomlFile} Config.toml
+
+            echo "Autogenerated file that implies that this server configuration is managed declaratively by NixOS" \
+              > .declarativeSettings
+
+          fi
+        '' else ''
+          if [ -e .declarativeSettings ]; then
+            rm .declarativeSettings
+          fi
+        '') + (if cfg.declarativeWhitelist then ''
+          if [ -e .declarativeWhitelist ]; then
+
+            # Whitelist was declarative before, no need to back up anything
+            ln -sf ${whitelistFile} whitelist.json
+
+          else
+
+            # Declarative whitelist for the first time, backup stateful files
+            ln -sb --suffix=.stateful ${whitelistFile} whitelist.json
+
+            echo "Autogenerated file that implies that this server's whitelist is managed declaratively by NixOS" \
+              > .declarativeWhitelist
+
+          fi
+        '' else ''
+          if [ -e .declarativeWhitelist ]; then
+            rm .declarativeWhitelist
+          fi
+        '');
+    };
+
+    networking.firewall = mkIf (cfg.declarativeSettings && cfg.openFirewall) {
+      allowedUDPPorts = [ cfg.settings.port ];
+      allowedTCPPorts = [ cfg.settings.port ];
+    };
+  };
+
+  meta.maintainers = with maintainers; [ gdd ];
+}
diff --git a/nixos/modules/services/games/minetest-server.nix b/nixos/modules/services/games/minetest-server.nix
index e8c96881673..8dc36015349 100644
--- a/nixos/modules/services/games/minetest-server.nix
+++ b/nixos/modules/services/games/minetest-server.nix
@@ -3,15 +3,52 @@
 with lib;
 
 let
+  CONTAINS_NEWLINE_RE = ".*\n.*";
+  # The following values are reserved as complete option values:
+  # { - start of a group.
+  # """ - start of a multi-line string.
+  RESERVED_VALUE_RE = "[[:space:]]*(\"\"\"|\\{)[[:space:]]*";
+  NEEDS_MULTILINE_RE = "${CONTAINS_NEWLINE_RE}|${RESERVED_VALUE_RE}";
+
+  # There is no way to encode """ on its own line in a Minetest config.
+  UNESCAPABLE_RE = ".*\n\"\"\"\n.*";
+
+  toConfMultiline = name: value:
+    assert lib.assertMsg
+      ((builtins.match UNESCAPABLE_RE value) == null)
+      ''""" can't be on its own line in a minetest config.'';
+    "${name} = \"\"\"\n${value}\n\"\"\"\n";
+
+  toConf = values:
+    lib.concatStrings
+      (lib.mapAttrsToList
+        (name: value: {
+          bool = "${name} = ${toString value}\n";
+          int = "${name} = ${toString value}\n";
+          null = "";
+          set = "${name} = {\n${toConf value}}\n";
+          string =
+            if (builtins.match NEEDS_MULTILINE_RE value) != null
+            then toConfMultiline name value
+            else "${name} = ${value}\n";
+        }.${builtins.typeOf value})
+        values);
+
   cfg   = config.services.minetest-server;
-  flag  = val: name: if val != null then "--${name} ${toString val} " else "";
+  flag  = val: name: lib.optionals (val != null) ["--${name}" "${toString val}"];
+
   flags = [
-    (flag cfg.gameId "gameid")
-    (flag cfg.world "world")
-    (flag cfg.configPath "config")
-    (flag cfg.logPath "logfile")
-    (flag cfg.port "port")
-  ];
+    "--server"
+  ]
+    ++ (
+      if cfg.configPath != null
+      then ["--config" cfg.configPath]
+      else ["--config" (builtins.toFile "minetest.conf" (toConf cfg.config))])
+    ++ (flag cfg.gameId "gameid")
+    ++ (flag cfg.world "world")
+    ++ (flag cfg.logPath "logfile")
+    ++ (flag cfg.port "port")
+    ++ cfg.extraArgs;
 in
 {
   options = {
@@ -55,6 +92,16 @@ in
         '';
       };
 
+      config = mkOption {
+        type = types.attrsOf types.anything;
+        default = {};
+        description = lib.mdDoc ''
+          Settings to add to the minetest config file.
+
+          This option is ignored if `configPath` is set.
+        '';
+      };
+
       logPath = mkOption {
         type        = types.nullOr types.path;
         default     = null;
@@ -75,6 +122,14 @@ in
           If set to null, the default 30000 will be used.
         '';
       };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = lib.mdDoc ''
+          Additional command line flags to pass to the minetest executable.
+        '';
+      };
     };
   };
 
@@ -100,7 +155,7 @@ in
       script = ''
         cd /var/lib/minetest
 
-        exec ${pkgs.minetest}/bin/minetest --server ${concatStrings flags}
+        exec ${pkgs.minetest}/bin/minetest ${lib.escapeShellArgs flags}
       '';
     };
   };
diff --git a/nixos/modules/services/hardware/asusd.nix b/nixos/modules/services/hardware/asusd.nix
index fba9b059bbb..ebbdea26c05 100644
--- a/nixos/modules/services/hardware/asusd.nix
+++ b/nixos/modules/services/hardware/asusd.nix
@@ -2,8 +2,6 @@
 
 let
   cfg = config.services.asusd;
-  json = pkgs.formats.json { };
-  toml = pkgs.formats.toml { };
 in
 {
   options = {
@@ -19,55 +17,55 @@ in
       };
 
       animeConfig = lib.mkOption {
-        type = json.type;
-        default = { };
+        type = lib.types.nullOr lib.types.str;
+        default = null;
         description = lib.mdDoc ''
-          The content of /etc/asusd/anime.conf.
+          The content of /etc/asusd/anime.ron.
           See https://asus-linux.org/asusctl/#anime-control.
         '';
       };
 
       asusdConfig = lib.mkOption {
-        type = json.type;
-        default = { };
+        type = lib.types.nullOr lib.types.str;
+        default = null;
         description = lib.mdDoc ''
-          The content of /etc/asusd/asusd.conf.
+          The content of /etc/asusd/asusd.ron.
           See https://asus-linux.org/asusctl/.
         '';
       };
 
       auraConfig = lib.mkOption {
-        type = json.type;
-        default = { };
+        type = lib.types.nullOr lib.types.str;
+        default = null;
         description = lib.mdDoc ''
-          The content of /etc/asusd/aura.conf.
+          The content of /etc/asusd/aura.ron.
           See https://asus-linux.org/asusctl/#led-keyboard-control.
         '';
       };
 
       profileConfig = lib.mkOption {
         type = lib.types.nullOr lib.types.str;
-        default = "";
+        default = null;
         description = lib.mdDoc ''
-          The content of /etc/asusd/profile.conf.
+          The content of /etc/asusd/profile.ron.
           See https://asus-linux.org/asusctl/#profiles.
         '';
       };
 
-      ledModesConfig = lib.mkOption {
-        type = lib.types.nullOr toml.type;
-        default = null;
-        description = lib.mdDoc ''
-          The content of /etc/asusd/asusd-ledmodes.toml. Leave `null` to use default settings.
-          See https://asus-linux.org/asusctl/#led-keyboard-control.
+      fanCurvesConfig = lib.mkOption {
+      type = lib.types.nullOr lib.types.str;
+      default = null;
+      description = lib.mdDoc ''
+          The content of /etc/asusd/fan_curves.ron.
+          See https://asus-linux.org/asusctl/#fan-curves.
         '';
       };
 
       userLedModesConfig = lib.mkOption {
-        type = lib.types.nullOr toml.type;
+        type = lib.types.nullOr lib.types.str;
         default = null;
         description = lib.mdDoc ''
-          The content of /etc/asusd/asusd-user-ledmodes.toml.
+          The content of /etc/asusd/asusd-user-ledmodes.ron.
           See https://asus-linux.org/asusctl/#led-keyboard-control.
         '';
       };
@@ -79,26 +77,18 @@ in
 
     environment.etc =
       let
-        maybeConfig = name: cfg: lib.mkIf (cfg != { }) {
-          source = json.generate name cfg;
+        maybeConfig = name: cfg: lib.mkIf (cfg != null) {
+          source = pkgs.writeText name cfg;
           mode = "0644";
         };
       in
       {
-        "asusd/anime.conf" = maybeConfig "anime.conf" cfg.animeConfig;
-        "asusd/asusd.conf" = maybeConfig "asusd.conf" cfg.asusdConfig;
-        "asusd/aura.conf" = maybeConfig "aura.conf" cfg.auraConfig;
-        "asusd/profile.conf" = lib.mkIf (cfg.profileConfig != null) {
-          source = pkgs.writeText "profile.conf" cfg.profileConfig;
-          mode = "0644";
-        };
-        "asusd/asusd-ledmodes.toml" = {
-          source =
-            if cfg.ledModesConfig == null
-            then "${pkgs.asusctl}/share/asusd/data/asusd-ledmodes.toml"
-            else toml.generate "asusd-ledmodes.toml" cfg.ledModesConfig;
-          mode = "0644";
-        };
+        "asusd/anime.ron" = maybeConfig "anime.ron" cfg.animeConfig;
+        "asusd/asusd.ron" = maybeConfig "asusd.ron" cfg.asusdConfig;
+        "asusd/aura.ron" = maybeConfig "aura.ron" cfg.auraConfig;
+        "asusd/profile.conf" = maybeConfig "profile.ron" cfg.profileConfig;
+        "asusd/fan_curves.ron" = maybeConfig "fan_curves.ron" cfg.fanCurvesConfig;
+        "asusd/asusd_user_ledmodes.ron" = maybeConfig "asusd_user_ledmodes.ron" cfg.userLedModesConfig;
       };
 
     services.dbus.enable = true;
diff --git a/nixos/modules/services/hardware/auto-cpufreq.nix b/nixos/modules/services/hardware/auto-cpufreq.nix
index 9698e72eb31..fd2e03ef12f 100644
--- a/nixos/modules/services/hardware/auto-cpufreq.nix
+++ b/nixos/modules/services/hardware/auto-cpufreq.nix
@@ -2,10 +2,26 @@
 with lib;
 let
   cfg = config.services.auto-cpufreq;
+  cfgFilename = "auto-cpufreq.conf";
+  cfgFile = format.generate cfgFilename cfg.settings;
+
+  format = pkgs.formats.ini {};
 in {
   options = {
     services.auto-cpufreq = {
       enable = mkEnableOption (lib.mdDoc "auto-cpufreq daemon");
+
+      settings = mkOption {
+        description = lib.mdDoc ''
+          Configuration for `auto-cpufreq`.
+
+          See its [example configuration file] for supported settings.
+          [example configuration file]: https://github.com/AdnanHodzic/auto-cpufreq/blob/master/auto-cpufreq.conf-example
+          '';
+
+        default = {};
+        type = types.submodule { freeformType = format.type; };
+      };
     };
   };
 
@@ -18,6 +34,11 @@ in {
         # Workaround for https://github.com/NixOS/nixpkgs/issues/81138
         wantedBy = [ "multi-user.target" ];
         path = with pkgs; [ bash coreutils ];
+
+        serviceConfig.ExecStart = [
+          ""
+          "${lib.getExe pkgs.auto-cpufreq} --daemon --config ${cfgFile}"
+        ];
       };
     };
   };
diff --git a/nixos/modules/services/hardware/bluetooth.nix b/nixos/modules/services/hardware/bluetooth.nix
index 6453e6968dc..2a58be51bb0 100644
--- a/nixos/modules/services/hardware/bluetooth.nix
+++ b/nixos/modules/services/hardware/bluetooth.nix
@@ -71,6 +71,29 @@ in
         };
         description = lib.mdDoc "Set configuration for system-wide bluetooth (/etc/bluetooth/main.conf).";
       };
+
+      input = mkOption {
+        type = cfgFmt.type;
+        default = { };
+        example = {
+          General = {
+            IdleTimeout = 30;
+            ClassicBondedOnly = true;
+          };
+        };
+        description = lib.mdDoc "Set configuration for the input service (/etc/bluetooth/input.conf).";
+      };
+
+      network = mkOption {
+        type = cfgFmt.type;
+        default = { };
+        example = {
+          General = {
+            DisableSecurity = true;
+          };
+        };
+        description = lib.mdDoc "Set configuration for the network service (/etc/bluetooth/network.conf).";
+      };
     };
   };
 
@@ -80,6 +103,10 @@ in
     environment.systemPackages = [ package ]
       ++ optional cfg.hsphfpd.enable pkgs.hsphfpd;
 
+    environment.etc."bluetooth/input.conf".source =
+      cfgFmt.generate "input.conf" cfg.input;
+    environment.etc."bluetooth/network.conf".source =
+      cfgFmt.generate "network.conf" cfg.network;
     environment.etc."bluetooth/main.conf".source =
       cfgFmt.generate "main.conf" (recursiveUpdate defaults cfg.settings);
     services.udev.packages = [ package ];
diff --git a/nixos/modules/services/hardware/fancontrol.nix b/nixos/modules/services/hardware/fancontrol.nix
index e7eb8ebf92b..993c37b2364 100644
--- a/nixos/modules/services/hardware/fancontrol.nix
+++ b/nixos/modules/services/hardware/fancontrol.nix
@@ -42,6 +42,13 @@ in
         ExecStart = "${pkgs.lm_sensors}/sbin/fancontrol ${configFile}";
       };
     };
+
+    # On some systems, the fancontrol service does not resume properly after sleep because the pwm status of the fans
+    # is not reset properly. Restarting the service fixes this, in accordance with https://github.com/lm-sensors/lm-sensors/issues/172.
+    powerManagement.resumeCommands = ''
+      systemctl restart fancontrol.service
+    '';
+
   };
 
   meta.maintainers = [ maintainers.evils ];
diff --git a/nixos/modules/services/hardware/fwupd.nix b/nixos/modules/services/hardware/fwupd.nix
index 8d7651f97c3..4e5913fd275 100644
--- a/nixos/modules/services/hardware/fwupd.nix
+++ b/nixos/modules/services/hardware/fwupd.nix
@@ -13,16 +13,13 @@ let
   };
 
   customEtc = {
-    "fwupd/daemon.conf" = {
-      source = format.generate "daemon.conf" {
+    "fwupd/fwupd.conf" = {
+      source = format.generate "fwupd.conf" {
         fwupd = cfg.daemonSettings;
+        uefi_capsule = cfg.uefiCapsuleSettings;
       };
-    };
-    "fwupd/uefi_capsule.conf" = {
-      source = pkgs.writeText "uefi_capsule.conf" ''
-        [uefi_capsule]
-        OverrideESPMountPoint=${config.boot.loader.efi.efiSysMountPoint}
-      '';
+      # fwupd tries to chmod the file if it doesn't have the right permissions
+      mode = "0640";
     };
   };
 
@@ -53,7 +50,7 @@ let
     # to install it because it would create a cyclic dependency between
     # the outputs. We also need to enable the remote,
     # which should not be done by default.
-    if cfg.enableTestRemote then (enableRemote cfg.package.installedTests "fwupd-tests") else {}
+    lib.optionalAttrs cfg.enableTestRemote (enableRemote cfg.package.installedTests "fwupd-tests")
   );
 
 in {
@@ -127,6 +124,16 @@ in {
                 List of plugins to be disabled.
               '';
             };
+
+            EspLocation = mkOption {
+              type = types.path;
+              default = config.boot.loader.efi.efiSysMountPoint;
+              defaultText = lib.literalExpression "config.boot.loader.efi.efiSysMountPoint";
+              description = lib.mdDoc ''
+                The EFI system partition (ESP) path used if UDisks is not available
+                or if this partition is not mounted at /boot/efi, /boot, or /efi
+              '';
+            };
           };
         };
         default = {};
@@ -134,6 +141,16 @@ in {
           Configurations for the fwupd daemon.
         '';
       };
+
+      uefiCapsuleSettings = mkOption {
+        type = types.submodule {
+          freeformType = format.type.nestedTypes.elemType;
+        };
+        default = {};
+        description = lib.mdDoc ''
+          UEFI capsule configurations for the fwupd daemon.
+        '';
+      };
     };
   };
 
@@ -147,7 +164,10 @@ in {
   ###### implementation
   config = mkIf cfg.enable {
     # Disable test related plug-ins implicitly so that users do not have to care about them.
-    services.fwupd.daemonSettings.DisabledPlugins = cfg.package.defaultDisabledPlugins;
+    services.fwupd.daemonSettings = {
+      DisabledPlugins = cfg.package.defaultDisabledPlugins;
+      EspLocation = config.boot.loader.efi.efiSysMountPoint;
+    };
 
     environment.systemPackages = [ cfg.package ];
 
@@ -158,6 +178,9 @@ in {
 
     services.udev.packages = [ cfg.package ];
 
+    # required to update the firmware of disks
+    services.udisks2.enable = true;
+
     systemd.packages = [ cfg.package ];
 
     security.polkit.enable = true;
diff --git a/nixos/modules/services/hardware/kanata.nix b/nixos/modules/services/hardware/kanata.nix
index 84265eb8f94..7d544050130 100644
--- a/nixos/modules/services/hardware/kanata.nix
+++ b/nixos/modules/services/hardware/kanata.nix
@@ -8,19 +8,9 @@ let
   keyboard = {
     options = {
       devices = mkOption {
-        type = types.addCheck (types.listOf types.str)
-          (devices: (length devices) > 0);
+        type = types.listOf types.str;
         example = [ "/dev/input/by-id/usb-0000_0000-event-kbd" ];
-        # TODO replace note with tip, which has not been implemented yet in
-        # nixos/lib/make-options-doc/mergeJSON.py
-        description = mdDoc ''
-          Paths to keyboard devices.
-
-          ::: {.note}
-          To avoid unnecessary triggers of the service unit, unplug devices in
-          the order of the list.
-          :::
-        '';
+        description = mdDoc "Paths to keyboard devices.";
       };
       config = mkOption {
         type = types.lines;
@@ -44,8 +34,10 @@ let
             cap (tap-hold 100 100 caps lctl))
         '';
         description = mdDoc ''
-          Configuration other than `defcfg`. See [example config
-          files](https://github.com/jtroo/kanata) for more information.
+          Configuration other than `defcfg`.
+
+          See [example config files](https://github.com/jtroo/kanata)
+          for more information.
         '';
       };
       extraDefCfg = mkOption {
@@ -53,8 +45,12 @@ let
         default = "";
         example = "danger-enable-cmd yes";
         description = mdDoc ''
-          Configuration of `defcfg` other than `linux-dev`. See [example
-          config files](https://github.com/jtroo/kanata) for more information.
+          Configuration of `defcfg` other than `linux-dev` (generated
+          from the devices option) and
+          `linux-continue-if-no-devs-found` (hardcoded to be yes).
+
+          See [example config files](https://github.com/jtroo/kanata)
+          for more information.
         '';
       };
       extraArgs = mkOption {
@@ -67,8 +63,7 @@ let
         default = null;
         example = 6666;
         description = mdDoc ''
-          Port to run the notification server on. `null` will not run the
-          server.
+          Port to run the TCP server on. `null` will not run the server.
         '';
       };
     };
@@ -76,28 +71,24 @@ let
 
   mkName = name: "kanata-${name}";
 
-  mkDevices = devices: concatStringsSep ":" devices;
+  mkDevices = devices:
+    optionalString ((length devices) > 0) "linux-dev ${concatStringsSep ":" devices}";
 
   mkConfig = name: keyboard: pkgs.writeText "${mkName name}-config.kdb" ''
     (defcfg
       ${keyboard.extraDefCfg}
-      linux-dev ${mkDevices keyboard.devices})
+      ${mkDevices keyboard.devices}
+      linux-continue-if-no-devs-found yes)
 
     ${keyboard.config}
   '';
 
   mkService = name: keyboard: nameValuePair (mkName name) {
-    description = "kanata for ${mkDevices keyboard.devices}";
-
-    # Because path units are used to activate service units, which
-    # will start the old stopped services during "nixos-rebuild
-    # switch", stopIfChanged here is a workaround to make sure new
-    # services are running after "nixos-rebuild switch".
-    stopIfChanged = false;
-
+    wantedBy = [ "multi-user.target" ];
     serviceConfig = {
+      Type = "notify";
       ExecStart = ''
-        ${cfg.package}/bin/kanata \
+        ${getExe cfg.package} \
           --cfg ${mkConfig name keyboard} \
           --symlink-path ''${RUNTIME_DIRECTORY}/${name} \
           ${optionalString (keyboard.port != null) "--port ${toString keyboard.port}"} \
@@ -133,8 +124,7 @@ let
       ProtectKernelModules = true;
       ProtectKernelTunables = true;
       ProtectProc = "invisible";
-      RestrictAddressFamilies =
-        if (keyboard.port == null) then "none" else [ "AF_INET" ];
+      RestrictAddressFamilies = [ "AF_UNIX" ] ++ optional (keyboard.port != null) "AF_INET";
       RestrictNamespaces = true;
       RestrictRealtime = true;
       SystemCallArchitectures = [ "native" ];
@@ -146,37 +136,10 @@ let
       UMask = "0077";
     };
   };
-
-  mkPathName = i: name: "${mkName name}-${toString i}";
-
-  mkPath = name: n: i: device:
-    nameValuePair (mkPathName i name) {
-      description =
-        "${toString (i+1)}/${toString n} kanata trigger for ${name}, watching ${device}";
-      wantedBy = optional (i == 0) "multi-user.target";
-      pathConfig = {
-        PathExists = device;
-        # (ab)use systemd.path to construct a trigger chain so that the
-        # service unit is only started when all paths exist
-        # however, manual of systemd.path says Unit's suffix is not ".path"
-        Unit =
-          if (i + 1) == n
-          then "${mkName name}.service"
-          else "${mkPathName (i + 1) name}.path";
-      };
-      unitConfig.StopPropagatedFrom = optional (i > 0) "${mkName name}.service";
-    };
-
-  mkPaths = name: keyboard:
-    let
-      n = length keyboard.devices;
-    in
-    imap0 (mkPath name n) keyboard.devices
-  ;
 in
 {
   options.services.kanata = {
-    enable = mkEnableOption (lib.mdDoc "kanata");
+    enable = mkEnableOption (mdDoc "kanata");
     package = mkOption {
       type = types.package;
       default = pkgs.kanata;
@@ -201,14 +164,7 @@ in
   config = mkIf cfg.enable {
     hardware.uinput.enable = true;
 
-    systemd = {
-      paths = trivial.pipe cfg.keyboards [
-        (mapAttrsToList mkPaths)
-        concatLists
-        listToAttrs
-      ];
-      services = mapAttrs' mkService cfg.keyboards;
-    };
+    systemd.services = mapAttrs' mkService cfg.keyboards;
   };
 
   meta.maintainers = with maintainers; [ linj ];
diff --git a/nixos/modules/services/hardware/keyd.nix b/nixos/modules/services/hardware/keyd.nix
new file mode 100644
index 00000000000..d17b0e4303e
--- /dev/null
+++ b/nixos/modules/services/hardware/keyd.nix
@@ -0,0 +1,126 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.keyd;
+  settingsFormat = pkgs.formats.ini { };
+in
+{
+  options = {
+    services.keyd = {
+      enable = mkEnableOption (lib.mdDoc "keyd, a key remapping daemon");
+
+      ids = mkOption {
+        type = types.listOf types.string;
+        default = [ "*" ];
+        example = [ "*" "-0123:0456" ];
+        description = lib.mdDoc ''
+          Device identifiers, as shown by {manpage}`keyd(1)`.
+        '';
+      };
+
+      settings = mkOption {
+        type = settingsFormat.type;
+        default = { };
+        example = {
+          main = {
+            capslock = "overload(control, esc)";
+            rightalt = "layer(rightalt)";
+          };
+
+          rightalt = {
+            j = "down";
+            k = "up";
+            h = "left";
+            l = "right";
+          };
+        };
+        description = lib.mdDoc ''
+          Configuration, except `ids` section, that is written to {file}`/etc/keyd/default.conf`.
+          See <https://github.com/rvaiya/keyd> how to configure.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."keyd/default.conf".source = pkgs.runCommand "default.conf"
+      {
+        ids = ''
+          [ids]
+          ${concatStringsSep "\n" cfg.ids}
+        '';
+        passAsFile = [ "ids" ];
+      } ''
+      cat $idsPath <(echo) ${settingsFormat.generate "keyd-main.conf" cfg.settings} >$out
+    '';
+
+    hardware.uinput.enable = lib.mkDefault true;
+
+    systemd.services.keyd = {
+      description = "Keyd remapping daemon";
+      documentation = [ "man:keyd(1)" ];
+
+      wantedBy = [ "multi-user.target" ];
+
+      restartTriggers = [
+        config.environment.etc."keyd/default.conf".source
+      ];
+
+      # this is configurable in 2.4.2, later versions seem to remove this option.
+      # post-2.4.2 may need to set makeFlags in the derivation:
+      #
+      #     makeFlags = [ "SOCKET_PATH/run/keyd/keyd.socket" ];
+      environment.KEYD_SOCKET = "/run/keyd/keyd.sock";
+
+      serviceConfig = {
+        ExecStart = "${pkgs.keyd}/bin/keyd";
+        Restart = "always";
+
+        # TODO investigate why it doesn't work propeprly with DynamicUser
+        # See issue: https://github.com/NixOS/nixpkgs/issues/226346
+        # DynamicUser = true;
+        SupplementaryGroups = [
+          config.users.groups.input.name
+          config.users.groups.uinput.name
+        ];
+
+        RuntimeDirectory = "keyd";
+
+        # Hardening
+        CapabilityBoundingSet = "";
+        DeviceAllow = [
+          "char-input rw"
+          "/dev/uinput rw"
+        ];
+        ProtectClock = true;
+        PrivateNetwork = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        PrivateUsers = true;
+        PrivateMounts = true;
+        PrivateTmp = true;
+        RestrictNamespaces = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        LockPersonality = true;
+        ProtectProc = "invisible";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+          "~@resources"
+        ];
+        RestrictAddressFamilies = [ "AF_UNIX" ];
+        RestrictSUIDSGID = true;
+        IPAddressDeny = [ "any" ];
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        ProcSubset = "pid";
+        UMask = "0077";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/openrgb.nix b/nixos/modules/services/hardware/openrgb.nix
index 12438f01e52..310615ecc53 100644
--- a/nixos/modules/services/hardware/openrgb.nix
+++ b/nixos/modules/services/hardware/openrgb.nix
@@ -34,14 +34,15 @@ in {
     services.udev.packages = [ cfg.package ];
 
     boot.kernelModules = [ "i2c-dev" ]
-     ++ lib.optionals (cfg.motherboard == "amd") [ "i2c-piix" ]
+     ++ lib.optionals (cfg.motherboard == "amd") [ "i2c-piix4" ]
      ++ lib.optionals (cfg.motherboard == "intel") [ "i2c-i801" ];
 
     systemd.services.openrgb = {
       description = "OpenRGB server daemon";
       wantedBy = [ "multi-user.target" ];
-
       serviceConfig = {
+        StateDirectory = "OpenRGB";
+        WorkingDirectory = "/var/lib/OpenRGB";
         ExecStart = "${cfg.package}/bin/openrgb --server --server-port ${toString cfg.server.port}";
         Restart = "always";
       };
diff --git a/nixos/modules/services/hardware/pcscd.nix b/nixos/modules/services/hardware/pcscd.nix
index a09c64645c4..a9e4998efe3 100644
--- a/nixos/modules/services/hardware/pcscd.nix
+++ b/nixos/modules/services/hardware/pcscd.nix
@@ -24,7 +24,6 @@ in
 
     plugins = mkOption {
       type = types.listOf types.package;
-      default = [ pkgs.ccid ];
       defaultText = literalExpression "[ pkgs.ccid ]";
       example = literalExpression "[ pkgs.pcsc-cyberjack ]";
       description = lib.mdDoc "Plugin packages to be used for PCSC-Lite.";
@@ -56,6 +55,8 @@ in
     environment.systemPackages = [ package ];
     systemd.packages = [ (getBin package) ];
 
+    services.pcscd.plugins = [ pkgs.ccid ];
+
     systemd.sockets.pcscd.wantedBy = [ "sockets.target" ];
 
     systemd.services.pcscd = {
diff --git a/nixos/modules/services/hardware/supergfxd.nix b/nixos/modules/services/hardware/supergfxd.nix
index df339e4ba01..bd82775e824 100644
--- a/nixos/modules/services/hardware/supergfxd.nix
+++ b/nixos/modules/services/hardware/supergfxd.nix
@@ -32,6 +32,7 @@ in
 
     systemd.packages = [ pkgs.supergfxctl ];
     systemd.services.supergfxd.wantedBy = [ "multi-user.target" ];
+    systemd.services.supergfxd.path = [ pkgs.kmod pkgs.pciutils ];
 
     services.dbus.packages = [ pkgs.supergfxctl ];
     services.udev.packages = [ pkgs.supergfxctl ];
diff --git a/nixos/modules/services/hardware/throttled.nix b/nixos/modules/services/hardware/throttled.nix
index 99735ff6519..afca24d976e 100644
--- a/nixos/modules/services/hardware/throttled.nix
+++ b/nixos/modules/services/hardware/throttled.nix
@@ -20,12 +20,12 @@ in {
   config = mkIf cfg.enable {
     systemd.packages = [ pkgs.throttled ];
     # The upstream package has this in Install, but that's not enough, see the NixOS manual
-    systemd.services.lenovo_fix.wantedBy = [ "multi-user.target" ];
+    systemd.services.throttled.wantedBy = [ "multi-user.target" ];
 
-    environment.etc."lenovo_fix.conf".source =
+    environment.etc."throttled.conf".source =
       if cfg.extraConfig != ""
-      then pkgs.writeText "lenovo_fix.conf" cfg.extraConfig
-      else "${pkgs.throttled}/etc/lenovo_fix.conf";
+      then pkgs.writeText "throttled.conf" cfg.extraConfig
+      else "${pkgs.throttled}/etc/throttled.conf";
 
     # Kernel 5.9 spams warnings whenever userspace writes to CPU MSRs.
     # See https://github.com/erpalma/throttled/issues/215
diff --git a/nixos/modules/services/hardware/trezord.md b/nixos/modules/services/hardware/trezord.md
new file mode 100644
index 00000000000..58c244a44bc
--- /dev/null
+++ b/nixos/modules/services/hardware/trezord.md
@@ -0,0 +1,17 @@
+# Trezor {#trezor}
+
+Trezor is an open-source cryptocurrency hardware wallet and security token
+allowing secure storage of private keys.
+
+It offers advanced features such U2F two-factor authorization, SSH login
+through
+[Trezor SSH agent](https://wiki.trezor.io/Apps:SSH_agent),
+[GPG](https://wiki.trezor.io/GPG) and a
+[password manager](https://wiki.trezor.io/Trezor_Password_Manager).
+For more information, guides and documentation, see <https://wiki.trezor.io>.
+
+To enable Trezor support, add the following to your {file}`configuration.nix`:
+
+    services.trezord.enable = true;
+
+This will add all necessary udev rules and start Trezor Bridge.
diff --git a/nixos/modules/services/hardware/trezord.nix b/nixos/modules/services/hardware/trezord.nix
index 70c1fd09860..b2217fc9712 100644
--- a/nixos/modules/services/hardware/trezord.nix
+++ b/nixos/modules/services/hardware/trezord.nix
@@ -8,7 +8,7 @@ in {
   ### docs
 
   meta = {
-    doc = ./trezord.xml;
+    doc = ./trezord.md;
   };
 
   ### interface
diff --git a/nixos/modules/services/hardware/trezord.xml b/nixos/modules/services/hardware/trezord.xml
deleted file mode 100644
index 972d409d9d0..00000000000
--- a/nixos/modules/services/hardware/trezord.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="trezor">
- <title>Trezor</title>
- <para>
-  Trezor is an open-source cryptocurrency hardware wallet and security token
-  allowing secure storage of private keys.
- </para>
- <para>
-  It offers advanced features such U2F two-factor authorization, SSH login
-  through
-  <link xlink:href="https://wiki.trezor.io/Apps:SSH_agent">Trezor SSH agent</link>,
-  <link xlink:href="https://wiki.trezor.io/GPG">GPG</link> and a
-  <link xlink:href="https://wiki.trezor.io/Trezor_Password_Manager">password manager</link>.
-  For more information, guides and documentation, see <link xlink:href="https://wiki.trezor.io"/>.
- </para>
- <para>
-  To enable Trezor support, add the following to your <filename>configuration.nix</filename>:
-<programlisting>
-<xref linkend="opt-services.trezord.enable"/> = true;
-</programlisting>
-  This will add all necessary udev rules and start Trezor Bridge.
- </para>
-</chapter>
diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix
index d9526133241..56120094871 100644
--- a/nixos/modules/services/hardware/udev.nix
+++ b/nixos/modules/services/hardware/udev.nix
@@ -16,16 +16,6 @@ let
   '';
 
 
-  # networkd link files are used early by udev to set up interfaces early.
-  # This must be done in stage 1 to avoid race conditions between udev and
-  # network daemons.
-  # TODO move this into the initrd-network module when it exists
-  initrdLinkUnits = pkgs.runCommand "initrd-link-units" {} ''
-    mkdir -p $out
-    ln -s ${udev}/lib/systemd/network/*.link $out/
-    ${lib.concatMapStringsSep "\n" (file: "ln -s ${file} $out/") (lib.mapAttrsToList (n: v: "${v.unit}/${n}") (lib.filterAttrs (n: _: hasSuffix ".link" n) config.systemd.network.units))}
-  '';
-
   extraUdevRules = pkgs.writeTextFile {
     name = "extra-udev-rules";
     text = cfg.extraRules;
@@ -82,7 +72,7 @@ let
           --replace \"/sbin/blkid \"${pkgs.util-linux}/sbin/blkid \
           --replace \"/bin/mount \"${pkgs.util-linux}/bin/mount \
           --replace /usr/bin/readlink ${pkgs.coreutils}/bin/readlink \
-          --replace /usr/bin/basename ${pkgs.coreutils}/bin/basename
+          --replace /usr/bin/basename ${pkgs.coreutils}/bin/basename 2>/dev/null
       ${optionalString (initrdBin != null) ''
         substituteInPlace $i --replace '/run/current-system/systemd' "${removeSuffix "/bin" initrdBin}"
       ''}
@@ -170,7 +160,7 @@ let
 
       echo "Generating hwdb database..."
       # hwdb --update doesn't return error code even on errors!
-      res="$(${pkgs.buildPackages.udev}/bin/udevadm hwdb --update --root=$(pwd) 2>&1)"
+      res="$(${pkgs.buildPackages.systemd}/bin/systemd-hwdb --root=$(pwd) update 2>&1)"
       echo "$res"
       [ -z "$(echo "$res" | egrep '^Error')" ]
       mv etc/udev/hwdb.bin $out
@@ -306,7 +296,6 @@ in
       packages = mkOption {
         type = types.listOf types.path;
         default = [];
-        visible = false;
         description = lib.mdDoc ''
           *This will only be used when systemd is used in stage 1.*
 
@@ -321,7 +310,6 @@ in
       binPackages = mkOption {
         type = types.listOf types.path;
         default = [];
-        visible = false;
         description = lib.mdDoc ''
           *This will only be used when systemd is used in stage 1.*
 
@@ -398,7 +386,6 @@ in
         systemd = config.boot.initrd.systemd.package;
         binPackages = config.boot.initrd.services.udev.binPackages ++ [ config.boot.initrd.systemd.contents."/bin".source ];
       };
-      "/etc/systemd/network".source = initrdLinkUnits;
     };
     # Insert initrd rules
     boot.initrd.services.udev.packages = [
diff --git a/nixos/modules/services/hardware/udisks2.nix b/nixos/modules/services/hardware/udisks2.nix
index 7368845dafd..c53dbf47774 100644
--- a/nixos/modules/services/hardware/udisks2.nix
+++ b/nixos/modules/services/hardware/udisks2.nix
@@ -1,10 +1,9 @@
 # Udisks daemon.
-
 { config, lib, pkgs, ... }:
-
 with lib;
 
 let
+  cfg = config.services.udisks2;
   settingsFormat = pkgs.formats.ini {
     listToValue = concatMapStringsSep "," (generators.mkValueStringDefault {});
   };
@@ -19,7 +18,17 @@ in
 
     services.udisks2 = {
 
-      enable = mkEnableOption (lib.mdDoc "udisks2, a DBus service that allows applications to query and manipulate storage devices.");
+      enable = mkEnableOption (mdDoc "udisks2, a DBus service that allows applications to query and manipulate storage devices");
+
+      mountOnMedia = mkOption {
+        type = types.bool;
+        default = false;
+        description = mdDoc ''
+          When enabled, instructs udisks2 to mount removable drives under `/media/` directory, instead of the
+          default, ACL-controlled `/run/media/$USER/`. Since `/media/` is not mounted as tmpfs by default, it
+          requires cleanup to get rid of stale mountpoints; enabling this option will take care of this at boot.
+        '';
+      };
 
       settings = mkOption rec {
         type = types.attrsOf settingsFormat.type;
@@ -44,7 +53,7 @@ in
           };
         };
         '';
-        description = lib.mdDoc ''
+        description = mdDoc ''
           Options passed to udisksd.
           See [here](http://manpages.ubuntu.com/manpages/latest/en/man5/udisks2.conf.5.html) and
           drive configuration in [here](http://manpages.ubuntu.com/manpages/latest/en/man8/udisks.8.html) for supported options.
@@ -73,10 +82,15 @@ in
 
     services.dbus.packages = [ pkgs.udisks2 ];
 
-    systemd.tmpfiles.rules = [ "d /var/lib/udisks2 0755 root root -" ];
+    systemd.tmpfiles.rules = [ "d /var/lib/udisks2 0755 root root -" ]
+      ++ optional cfg.mountOnMedia "D! /media 0755 root root -";
 
     services.udev.packages = [ pkgs.udisks2 ];
 
+    services.udev.extraRules = optionalString cfg.mountOnMedia ''
+      ENV{ID_FS_USAGE}=="filesystem", ENV{UDISKS_FILESYSTEM_SHARED}="1"
+    '';
+
     systemd.packages = [ pkgs.udisks2 ];
   };
 
diff --git a/nixos/modules/services/hardware/undervolt.nix b/nixos/modules/services/hardware/undervolt.nix
index c49d944cdc1..94477747540 100644
--- a/nixos/modules/services/hardware/undervolt.nix
+++ b/nixos/modules/services/hardware/undervolt.nix
@@ -5,8 +5,8 @@ let
   cfg = config.services.undervolt;
 
   mkPLimit = limit: window:
-    if (isNull limit && isNull window) then null
-    else assert asserts.assertMsg (!isNull limit && !isNull window) "Both power limit and window must be set";
+    if (limit == null && window == null) then null
+    else assert asserts.assertMsg (limit != null && window != null) "Both power limit and window must be set";
       "${toString limit} ${toString window}";
   cliArgs = lib.cli.toGNUCommandLine {} {
     inherit (cfg)
diff --git a/nixos/modules/services/home-automation/esphome.nix b/nixos/modules/services/home-automation/esphome.nix
new file mode 100644
index 00000000000..d7dbb6f0b90
--- /dev/null
+++ b/nixos/modules/services/home-automation/esphome.nix
@@ -0,0 +1,136 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib)
+    literalExpression
+    maintainers
+    mkEnableOption
+    mkIf
+    mkOption
+    mdDoc
+    types
+    ;
+
+  cfg = config.services.esphome;
+
+  stateDir = "/var/lib/esphome";
+
+  esphomeParams =
+    if cfg.enableUnixSocket
+    then "--socket /run/esphome/esphome.sock"
+    else "--address ${cfg.address} --port ${toString cfg.port}";
+in
+{
+  meta.maintainers = with maintainers; [ oddlama ];
+
+  options.services.esphome = {
+    enable = mkEnableOption (mdDoc "esphome");
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.esphome;
+      defaultText = literalExpression "pkgs.esphome";
+      description = mdDoc "The package to use for the esphome command.";
+    };
+
+    enableUnixSocket = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc "Listen on a unix socket `/run/esphome/esphome.sock` instead of the TCP port.";
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = mdDoc "esphome address";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 6052;
+      description = mdDoc "esphome port";
+    };
+
+    openFirewall = mkOption {
+      default = false;
+      type = types.bool;
+      description = mdDoc "Whether to open the firewall for the specified port.";
+    };
+
+    allowedDevices = mkOption {
+      default = ["char-ttyS" "char-ttyUSB"];
+      example = ["/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0"];
+      description = lib.mdDoc ''
+        A list of device nodes to which {command}`esphome` has access to.
+        Refer to DeviceAllow in systemd.resource-control(5) for more information.
+        Beware that if a device is referred to by an absolute path instead of a device category,
+        it will only allow devices that already are plugged in when the service is started.
+      '';
+      type = types.listOf types.str;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall && !cfg.enableUnixSocket) [cfg.port];
+
+    systemd.services.esphome = {
+      description = "ESPHome dashboard";
+      after = ["network.target"];
+      wantedBy = ["multi-user.target"];
+      path = [cfg.package];
+
+      # platformio fails to determine the home directory when using DynamicUser
+      environment.PLATFORMIO_CORE_DIR = "${stateDir}/.platformio";
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/esphome dashboard ${esphomeParams} ${stateDir}";
+        DynamicUser = true;
+        User = "esphome";
+        Group = "esphome";
+        WorkingDirectory = stateDir;
+        StateDirectory = "esphome";
+        StateDirectoryMode = "0750";
+        Restart = "on-failure";
+        RuntimeDirectory = mkIf cfg.enableUnixSocket "esphome";
+        RuntimeDirectoryMode = "0750";
+
+        # Hardening
+        CapabilityBoundingSet = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        DevicePolicy = "closed";
+        DeviceAllow = map (d: "${d} rw") cfg.allowedDevices;
+        SupplementaryGroups = ["dialout"];
+        #NoNewPrivileges = true; # Implied by DynamicUser
+        PrivateUsers = true;
+        #PrivateTmp = true; # Implied by DynamicUser
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        ProtectSystem = "strict";
+        #RemoveIPC = true; # Implied by DynamicUser
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_NETLINK"
+          "AF_UNIX"
+        ];
+        RestrictNamespaces = false; # Required by platformio for chroot
+        RestrictRealtime = true;
+        #RestrictSUIDSGID = true; # Implied by DynamicUser
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "@mount" # Required by platformio for chroot
+        ];
+        UMask = "0077";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/home-automation/evcc.nix b/nixos/modules/services/home-automation/evcc.nix
index efa2cf24431..d0ce3fb4a1c 100644
--- a/nixos/modules/services/home-automation/evcc.nix
+++ b/nixos/modules/services/home-automation/evcc.nix
@@ -50,7 +50,7 @@ in
       ];
       environment.HOME = "/var/lib/evcc";
       path = with pkgs; [
-        glibc # requires getent
+        getent
       ];
       serviceConfig = {
         ExecStart = "${package}/bin/evcc --config ${configFile} ${escapeShellArgs cfg.extraArgs}";
diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix
index fa06e5391bb..abe0b93e412 100644
--- a/nixos/modules/services/home-automation/home-assistant.nix
+++ b/nixos/modules/services/home-automation/home-assistant.nix
@@ -35,7 +35,10 @@ let
   #   ...
   # } ];
   usedPlatforms = config:
-    if isAttrs config then
+    # don't recurse into derivations possibly creating an infinite recursion
+    if isDerivation config then
+      [ ]
+    else if isAttrs config then
       optional (config ? platform) config.platform
       ++ concatMap usedPlatforms (attrValues config)
     else if isList config then
@@ -362,7 +365,7 @@ in {
   config = mkIf cfg.enable {
     assertions = [
       {
-        assertion = cfg.openFirewall -> !isNull cfg.config;
+        assertion = cfg.openFirewall -> cfg.config != null;
         message = "openFirewall can only be used with a declarative config";
       }
     ];
@@ -409,6 +412,7 @@ in {
         (optionalString (cfg.config != null) copyConfig) +
         (optionalString (cfg.lovelaceConfig != null) copyLovelaceConfig)
       ;
+      environment.PYTHONPATH = package.pythonPath;
       serviceConfig = let
         # List of capabilities to equip home-assistant with, depending on configured components
         capabilities = lib.unique ([
@@ -438,11 +442,13 @@ in {
           "aranet"
           "bluemaestro"
           "bluetooth"
+          "bluetooth_adapters"
           "bluetooth_le_tracker"
           "bluetooth_tracker"
           "bthome"
           "default_config"
           "eq3btsmart"
+          "eufylife_ble"
           "esphome"
           "fjaraskupan"
           "govee_ble"
@@ -452,8 +458,11 @@ in {
           "led_ble"
           "melnor"
           "moat"
+          "mopeka"
           "oralb"
           "qingping"
+          "rapt_ble"
+          "ruuvi_gateway"
           "ruuvitag_ble"
           "sensirion_ble"
           "sensorpro"
@@ -500,6 +509,7 @@ in {
           "mysensors"
           "nad"
           "numato"
+          "otbr"
           "rflink"
           "rfxtrx"
           "scsgate"
diff --git a/nixos/modules/services/logging/graylog.nix b/nixos/modules/services/logging/graylog.nix
index 70c3ca50888..1eb51c50ff7 100644
--- a/nixos/modules/services/logging/graylog.nix
+++ b/nixos/modules/services/logging/graylog.nix
@@ -37,8 +37,8 @@ in
 
       package = mkOption {
         type = types.package;
-        default = pkgs.graylog;
-        defaultText = literalExpression "pkgs.graylog";
+        default = if versionOlder config.system.stateVersion "23.05" then pkgs.graylog-3_3 else pkgs.graylog-5_0;
+        defaultText = literalExpression (if versionOlder config.system.stateVersion "23.05" then "pkgs.graylog-3_3" else "pkgs.graylog-5_0");
         description = lib.mdDoc "Graylog package to use.";
       };
 
diff --git a/nixos/modules/services/logging/logrotate.nix b/nixos/modules/services/logging/logrotate.nix
index 1799e9282b3..342ac5ec6e0 100644
--- a/nixos/modules/services/logging/logrotate.nix
+++ b/nixos/modules/services/logging/logrotate.nix
@@ -83,9 +83,8 @@ let
   };
 
   mailOption =
-    if foldr (n: a: a || (n.mail or false) != false) false (attrValues cfg.settings)
-    then "--mail=${pkgs.mailutils}/bin/mail"
-    else "";
+    optionalString (foldr (n: a: a || (n.mail or false) != false) false (attrValues cfg.settings))
+    "--mail=${pkgs.mailutils}/bin/mail";
 in
 {
   imports = [
@@ -187,7 +186,7 @@ in
           A configuration file automatically generated by NixOS.
         '';
         description = lib.mdDoc ''
-          Override the configuration file used by MySQL. By default,
+          Override the configuration file used by logrotate. By default,
           NixOS generates one automatically from [](#opt-services.logrotate.settings).
         '';
         example = literalExpression ''
diff --git a/nixos/modules/services/logging/syslogd.nix b/nixos/modules/services/logging/syslogd.nix
index 43969402588..553973e255f 100644
--- a/nixos/modules/services/logging/syslogd.nix
+++ b/nixos/modules/services/logging/syslogd.nix
@@ -7,7 +7,7 @@ let
   cfg = config.services.syslogd;
 
   syslogConf = pkgs.writeText "syslog.conf" ''
-    ${if (cfg.tty != "") then "kern.warning;*.err;authpriv.none /dev/${cfg.tty}" else ""}
+    ${optionalString (cfg.tty != "") "kern.warning;*.err;authpriv.none /dev/${cfg.tty}"}
     ${cfg.defaultConfig}
     ${cfg.extraConfig}
   '';
diff --git a/nixos/modules/services/logging/vector.nix b/nixos/modules/services/logging/vector.nix
index 1803ea85e49..f2edeabfc06 100644
--- a/nixos/modules/services/logging/vector.nix
+++ b/nixos/modules/services/logging/vector.nix
@@ -8,6 +8,8 @@ in
   options.services.vector = {
     enable = mkEnableOption (lib.mdDoc "Vector");
 
+    package = mkPackageOptionMD pkgs "vector" { };
+
     journaldAccess = mkOption {
       type = types.bool;
       default = false;
@@ -26,13 +28,9 @@ in
   };
 
   config = mkIf cfg.enable {
+    # for cli usage
+    environment.systemPackages = [ pkgs.vector ];
 
-    users.groups.vector = { };
-    users.users.vector = {
-      description = "Vector service user";
-      group = "vector";
-      isSystemUser = true;
-    };
     systemd.services.vector = {
       description = "Vector event and log aggregator";
       wantedBy = [ "multi-user.target" ];
@@ -51,9 +49,8 @@ in
             '';
         in
         {
-          ExecStart = "${pkgs.vector}/bin/vector --config ${validateConfig conf}";
-          User = "vector";
-          Group = "vector";
+          ExecStart = "${getExe cfg.package} --config ${validateConfig conf}";
+          DynamicUser = true;
           Restart = "no";
           StateDirectory = "vector";
           ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
diff --git a/nixos/modules/services/mail/davmail.nix b/nixos/modules/services/mail/davmail.nix
index 483f591a726..9cdb435af4a 100644
--- a/nixos/modules/services/mail/davmail.nix
+++ b/nixos/modules/services/mail/davmail.nix
@@ -91,6 +91,33 @@ in
           Restart = "on-failure";
           DynamicUser = "yes";
           LogsDirectory = "davmail";
+
+          CapabilityBoundingSet = [ "" ];
+          DeviceAllow = [ "" ];
+          LockPersonality = true;
+          NoNewPrivileges = true;
+          PrivateDevices = true;
+          PrivateTmp = true;
+          PrivateUsers = true;
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectSystem = "strict";
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          RemoveIPC = true;
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = "@system-service";
+          SystemCallErrorNumber = "EPERM";
+          UMask = "0077";
+
         };
       };
 
diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix
index f6a167572f7..21bafd859c3 100644
--- a/nixos/modules/services/mail/dovecot.nix
+++ b/nixos/modules/services/mail/dovecot.nix
@@ -171,11 +171,11 @@ in
   options.services.dovecot2 = {
     enable = mkEnableOption (lib.mdDoc "the dovecot 2.x POP3/IMAP server");
 
-    enablePop3 = mkEnableOption (lib.mdDoc "starting the POP3 listener (when Dovecot is enabled).");
+    enablePop3 = mkEnableOption (lib.mdDoc "starting the POP3 listener (when Dovecot is enabled)");
 
-    enableImap = mkEnableOption (lib.mdDoc "starting the IMAP listener (when Dovecot is enabled).") // { default = true; };
+    enableImap = mkEnableOption (lib.mdDoc "starting the IMAP listener (when Dovecot is enabled)") // { default = true; };
 
-    enableLmtp = mkEnableOption (lib.mdDoc "starting the LMTP listener (when Dovecot is enabled).");
+    enableLmtp = mkEnableOption (lib.mdDoc "starting the LMTP listener (when Dovecot is enabled)");
 
     protocols = mkOption {
       type = types.listOf types.str;
@@ -300,9 +300,9 @@ in
       description = lib.mdDoc "Path to the server's private key.";
     };
 
-    enablePAM = mkEnableOption (lib.mdDoc "creating a own Dovecot PAM service and configure PAM user logins.") // { default = true; };
+    enablePAM = mkEnableOption (lib.mdDoc "creating a own Dovecot PAM service and configure PAM user logins") // { default = true; };
 
-    enableDHE = mkEnableOption (lib.mdDoc "enable ssl_dh and generation of primes for the key exchange.") // { default = true; };
+    enableDHE = mkEnableOption (lib.mdDoc "enable ssl_dh and generation of primes for the key exchange") // { default = true; };
 
     sieveScripts = mkOption {
       type = types.attrsOf types.path;
@@ -310,7 +310,7 @@ in
       description = lib.mdDoc "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc.";
     };
 
-    showPAMFailure = mkEnableOption (lib.mdDoc "showing the PAM failure message on authentication error (useful for OTPW).");
+    showPAMFailure = mkEnableOption (lib.mdDoc "showing the PAM failure message on authentication error (useful for OTPW)");
 
     mailboxes = mkOption {
       type = with types; coercedTo
@@ -326,7 +326,7 @@ in
       description = lib.mdDoc "Configure mailboxes and auto create or subscribe them.";
     };
 
-    enableQuota = mkEnableOption (lib.mdDoc "the dovecot quota service.");
+    enableQuota = mkEnableOption (lib.mdDoc "the dovecot quota service");
 
     quotaPort = mkOption {
       type = types.str;
diff --git a/nixos/modules/services/mail/exim.nix b/nixos/modules/services/mail/exim.nix
index a9504acee35..1d1258913b6 100644
--- a/nixos/modules/services/mail/exim.nix
+++ b/nixos/modules/services/mail/exim.nix
@@ -116,8 +116,8 @@ in
       wantedBy = [ "multi-user.target" ];
       restartTriggers = [ config.environment.etc."exim.conf".source ];
       serviceConfig = {
-        ExecStart   = "+${cfg.package}/bin/exim -bdf -q${cfg.queueRunnerInterval}";
-        ExecReload  = "+${coreutils}/bin/kill -HUP $MAINPID";
+        ExecStart   = "!${cfg.package}/bin/exim -bdf -q${cfg.queueRunnerInterval}";
+        ExecReload  = "!${coreutils}/bin/kill -HUP $MAINPID";
         User        = cfg.user;
       };
       preStart = ''
diff --git a/nixos/modules/services/mail/goeland.nix b/nixos/modules/services/mail/goeland.nix
new file mode 100644
index 00000000000..13092a65ed9
--- /dev/null
+++ b/nixos/modules/services/mail/goeland.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.goeland;
+  tomlFormat = pkgs.formats.toml { };
+in
+{
+  options.services.goeland = {
+    enable = mkEnableOption (mdDoc "goeland");
+
+    settings = mkOption {
+      description = mdDoc ''
+        Configuration of goeland.
+        See the [example config file](https://github.com/slurdge/goeland/blob/master/cmd/asset/config.default.toml) for the available options.
+      '';
+      default = { };
+      type = tomlFormat.type;
+    };
+    schedule = mkOption {
+      type = types.str;
+      default = "12h";
+      example = "Mon, 00:00:00";
+      description = mdDoc "How often to run goeland, in systemd time format.";
+    };
+    stateDir = mkOption {
+      type = types.path;
+      default = "/var/lib/goeland";
+      description = mdDoc ''
+        The data directory for goeland where the database will reside if using the unseen filter.
+        If left as the default value this directory will automatically be created before the goeland
+        server starts, otherwise you are responsible for ensuring the directory exists with
+        appropriate ownership and permissions.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.goeland.settings.database = "${cfg.stateDir}/goeland.db";
+
+    systemd.services.goeland = {
+      serviceConfig = let confFile = tomlFormat.generate "config.toml" cfg.settings; in mkMerge [
+        {
+          ExecStart = "${pkgs.goeland}/bin/goeland run -c ${confFile}";
+          User = "goeland";
+          Group = "goeland";
+        }
+        (mkIf (cfg.stateDir == "/var/lib/goeland") {
+          StateDirectory = "goeland";
+          StateDirectoryMode = "0750";
+        })
+      ];
+      startAt = cfg.schedule;
+    };
+
+    users.users.goeland = {
+      description = "goeland user";
+      group = "goeland";
+      isSystemUser = true;
+    };
+    users.groups.goeland = { };
+
+    warnings = optionals (hasAttr "password" cfg.settings.email) [
+      ''
+        It is not recommended to set the "services.goeland.settings.email.password"
+        option as it will be in cleartext in the Nix store.
+        Please use "services.goeland.settings.email.password_file" instead.
+      ''
+    ];
+  };
+
+  meta.maintainers = with maintainers; [ sweenu ];
+}
diff --git a/nixos/modules/services/mail/listmonk.nix b/nixos/modules/services/mail/listmonk.nix
index 8b636bd5b1f..251362fdd89 100644
--- a/nixos/modules/services/mail/listmonk.nix
+++ b/nixos/modules/services/mail/listmonk.nix
@@ -128,7 +128,7 @@ in {
           '';
         };
       };
-      package = mkPackageOption pkgs "listmonk" {};
+      package = mkPackageOptionMD pkgs "listmonk" {};
       settings = mkOption {
         type = types.submodule { freeformType = tomlFormat.type; };
         description = lib.mdDoc ''
diff --git a/nixos/modules/services/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix
index eeb113e204c..3b4a517fb85 100644
--- a/nixos/modules/services/mail/maddy.nix
+++ b/nixos/modules/services/mail/maddy.nix
@@ -13,8 +13,6 @@ let
     # configuration here https://github.com/foxcpp/maddy/blob/master/maddy.conf
     # Do not use this in production!
 
-    tls off
-
     auth.pass_table local_authdb {
       table sql_table {
         driver sqlite3
@@ -35,6 +33,7 @@ let
       }
       optional_step file /etc/maddy/aliases
     }
+
     msgpipeline local_routing {
       destination postmaster $(local_domains) {
         modify {
@@ -207,7 +206,7 @@ in {
           Server configuration, see
           [https://maddy.email](https://maddy.email) for
           more information. The default configuration of this module will setup
-          minimal maddy instance for mail transfer without TLS encryption.
+          minimal Maddy instance for mail transfer without TLS encryption.
 
           ::: {.note}
           This should not be used in a production environment.
@@ -215,6 +214,76 @@ in {
         '';
       };
 
+      tls = {
+        loader = mkOption {
+          type = with types; nullOr (enum [ "off" "file" "acme" ]);
+          default = "off";
+          description = lib.mdDoc ''
+            TLS certificates are obtained by modules called "certificate
+            loaders".
+
+            The `file` loader module reads certificates from files specified by
+            the `certificates` option.
+
+            Alternatively the `acme` module can be used to automatically obtain
+            certificates using the ACME protocol.
+
+            Module configuration is done via the `tls.extraConfig` option.
+
+            Secrets such as API keys or passwords should not be supplied in
+            plaintext. Instead the `secrets` option can be used to read secrets
+            at runtime as environment variables. Secrets can be referenced with
+            `{env:VAR}`.
+          '';
+        };
+
+        certificates = mkOption {
+          type = with types; listOf (submodule {
+            options = {
+              keyPath = mkOption {
+                type = types.path;
+                example = "/etc/ssl/mx1.example.org.key";
+                description = lib.mdDoc ''
+                  Path to the private key used for TLS.
+                '';
+              };
+              certPath = mkOption {
+                type = types.path;
+                example = "/etc/ssl/mx1.example.org.crt";
+                description = lib.mdDoc ''
+                  Path to the certificate used for TLS.
+                '';
+              };
+            };
+          });
+          default = [];
+          example = lib.literalExpression ''
+            [{
+              keyPath = "/etc/ssl/mx1.example.org.key";
+              certPath = "/etc/ssl/mx1.example.org.crt";
+            }]
+          '';
+          description = lib.mdDoc ''
+            A list of attribute sets containing paths to TLS certificates and
+            keys. Maddy will use SNI if multiple pairs are selected.
+          '';
+        };
+
+        extraConfig = mkOption {
+          type = with types; nullOr lines;
+          description = lib.mdDoc ''
+            Arguments for the specified certificate loader.
+
+            In case the `tls` loader is set, the defaults are considered secure
+            and there is no need to change anything in most cases.
+            For available options see [upstream manual](https://maddy.email/reference/tls/).
+
+            For ACME configuration, see [following page](https://maddy.email/reference/tls-acme).
+          '';
+          default = "";
+        };
+      };
+
       openFirewall = mkOption {
         type = types.bool;
         default = false;
@@ -223,22 +292,126 @@ in {
         '';
       };
 
+      ensureAccounts = mkOption {
+        type = with types; listOf str;
+        default = [];
+        description = lib.mdDoc ''
+          List of IMAP accounts which get automatically created. Note that for
+          a complete setup, user credentials for these accounts are required
+          and can be created using the `ensureCredentials` option.
+          This option does not delete accounts which are not (anymore) listed.
+        '';
+        example = [
+          "user1@localhost"
+          "user2@localhost"
+        ];
+      };
+
+      ensureCredentials = mkOption {
+        default = {};
+        description = lib.mdDoc ''
+          List of user accounts which get automatically created if they don't
+          exist yet. Note that for a complete setup, corresponding mail boxes
+          have to get created using the `ensureAccounts` option.
+          This option does not delete accounts which are not (anymore) listed.
+        '';
+        example = {
+          "user1@localhost".passwordFile = /secrets/user1-localhost;
+          "user2@localhost".passwordFile = /secrets/user2-localhost;
+        };
+        type = types.attrsOf (types.submodule {
+          options = {
+            passwordFile = mkOption {
+              type = types.path;
+              example = "/path/to/file";
+              default = null;
+              description = lib.mdDoc ''
+                Specifies the path to a file containing the
+                clear text password for the user.
+              '';
+            };
+          };
+        });
+      };
+
+      secrets = lib.mkOption {
+        type = with types; listOf path;
+        description = lib.mdDoc ''
+          A list of files containing the various secrets. Should be in the format
+          expected by systemd's `EnvironmentFile` directory. Secrets can be
+          referenced in the format `{env:VAR}`.
+        '';
+        default = [ ];
+      };
+
     };
   };
 
   config = mkIf cfg.enable {
 
+    assertions = [
+      {
+        assertion = cfg.tls.loader == "file" -> cfg.tls.certificates != [];
+        message = ''
+          If Maddy is configured to use TLS, tls.certificates with attribute sets
+          of certPath and keyPath must be provided.
+          Read more about obtaining TLS certificates here:
+          https://maddy.email/tutorials/setting-up/#tls-certificates
+        '';
+      }
+      {
+        assertion = cfg.tls.loader == "acme" -> cfg.tls.extraConfig != "";
+        message = ''
+          If Maddy is configured to obtain TLS certificates using the ACME
+          loader, extra configuration options must be supplied via
+          tls.extraConfig option.
+          See upstream documentation for more details:
+          https://maddy.email/reference/tls-acme
+        '';
+      }
+    ];
+
     systemd = {
+
       packages = [ pkgs.maddy ];
-      services.maddy = {
-        serviceConfig = {
-          User = cfg.user;
-          Group = cfg.group;
-          StateDirectory = [ "maddy" ];
+      services = {
+        maddy = {
+          serviceConfig = {
+            User = cfg.user;
+            Group = cfg.group;
+            StateDirectory = [ "maddy" ];
+            EnvironmentFile = cfg.secrets;
+          };
+          restartTriggers = [ config.environment.etc."maddy/maddy.conf".source ];
+          wantedBy = [ "multi-user.target" ];
+        };
+        maddy-ensure-accounts = {
+          script = ''
+            ${optionalString (cfg.ensureAccounts != []) ''
+              ${concatMapStrings (account: ''
+                if ! ${pkgs.maddy}/bin/maddyctl imap-acct list | grep "${account}"; then
+                  ${pkgs.maddy}/bin/maddyctl imap-acct create ${account}
+                fi
+              '') cfg.ensureAccounts}
+            ''}
+            ${optionalString (cfg.ensureCredentials != {}) ''
+              ${concatStringsSep "\n" (mapAttrsToList (name: cfg: ''
+                if ! ${pkgs.maddy}/bin/maddyctl creds list | grep "${name}"; then
+                  ${pkgs.maddy}/bin/maddyctl creds create --password $(cat ${escapeShellArg cfg.passwordFile}) ${name}
+                fi
+              '') cfg.ensureCredentials)}
+            ''}
+          '';
+          serviceConfig = {
+            Type = "oneshot";
+            User= "maddy";
+          };
+          after = [ "maddy.service" ];
+          wantedBy = [ "multi-user.target" ];
         };
-        restartTriggers = [ config.environment.etc."maddy/maddy.conf".source ];
-        wantedBy = [ "multi-user.target" ];
+
       };
+
     };
 
     environment.etc."maddy/maddy.conf" = {
@@ -247,6 +420,23 @@ in {
         $(primary_domain) = ${cfg.primaryDomain}
         $(local_domains) = ${toString cfg.localDomains}
         hostname ${cfg.hostname}
+
+        ${if (cfg.tls.loader == "file") then ''
+          tls file ${concatStringsSep " " (
+            map (x: x.certPath + " " + x.keyPath
+          ) cfg.tls.certificates)} ${optionalString (cfg.tls.extraConfig != "") ''
+            { ${cfg.tls.extraConfig} }
+          ''}
+        '' else if (cfg.tls.loader == "acme") then ''
+          tls {
+            loader acme {
+              ${cfg.tls.extraConfig}
+            }
+          }
+        '' else if (cfg.tls.loader == "off") then ''
+          tls off
+        '' else ""}
+
         ${cfg.config}
       '';
     };
diff --git a/nixos/modules/services/mail/mailman.md b/nixos/modules/services/mail/mailman.md
new file mode 100644
index 00000000000..55b61f8a258
--- /dev/null
+++ b/nixos/modules/services/mail/mailman.md
@@ -0,0 +1,82 @@
+# Mailman {#module-services-mailman}
+
+[Mailman](https://www.list.org) is free
+software for managing electronic mail discussion and e-newsletter
+lists. Mailman and its web interface can be configured using the
+corresponding NixOS module. Note that this service is best used with
+an existing, securely configured Postfix setup, as it does not automatically configure this.
+
+## Basic usage with Postfix {#module-services-mailman-basic-usage}
+
+For a basic configuration with Postfix as the MTA, the following settings are suggested:
+```
+{ config, ... }: {
+  services.postfix = {
+    enable = true;
+    relayDomains = ["hash:/var/lib/mailman/data/postfix_domains"];
+    sslCert = config.security.acme.certs."lists.example.org".directory + "/full.pem";
+    sslKey = config.security.acme.certs."lists.example.org".directory + "/key.pem";
+    config = {
+      transport_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"];
+      local_recipient_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"];
+    };
+  };
+  services.mailman = {
+    enable = true;
+    serve.enable = true;
+    hyperkitty.enable = true;
+    webHosts = ["lists.example.org"];
+    siteOwner = "mailman@example.org";
+  };
+  services.nginx.virtualHosts."lists.example.org".enableACME = true;
+  networking.firewall.allowedTCPPorts = [ 25 80 443 ];
+}
+```
+
+DNS records will also be required:
+
+  - `AAAA` and `A` records pointing to the host in question, in order for browsers to be able to discover the address of the web server;
+  - An `MX` record pointing to a domain name at which the host is reachable, in order for other mail servers to be able to deliver emails to the mailing lists it hosts.
+
+After this has been done and appropriate DNS records have been
+set up, the Postorius mailing list manager and the Hyperkitty
+archive browser will be available at
+https://lists.example.org/. Note that this setup is not
+sufficient to deliver emails to most email providers nor to
+avoid spam -- a number of additional measures for authenticating
+incoming and outgoing mails, such as SPF, DMARC and DKIM are
+necessary, but outside the scope of the Mailman module.
+
+## Using with other MTAs {#module-services-mailman-other-mtas}
+
+Mailman also supports other MTA, though with a little bit more configuration. For example, to use Mailman with Exim, you can use the following settings:
+```
+{ config, ... }: {
+  services = {
+    mailman = {
+      enable = true;
+      siteOwner = "mailman@example.org";
+      enablePostfix = false;
+      settings.mta = {
+        incoming = "mailman.mta.exim4.LMTP";
+        outgoing = "mailman.mta.deliver.deliver";
+        lmtp_host = "localhost";
+        lmtp_port = "8024";
+        smtp_host = "localhost";
+        smtp_port = "25";
+        configuration = "python:mailman.config.exim4";
+      };
+    };
+    exim = {
+      enable = true;
+      # You can configure Exim in a separate file to reduce configuration.nix clutter
+      config = builtins.readFile ./exim.conf;
+    };
+  };
+}
+```
+
+The exim config needs some special additions to work with Mailman. Currently
+NixOS can't manage Exim config with such granularity. Please refer to
+[Mailman documentation](https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html)
+for more info on configuring Mailman for working with Exim.
diff --git a/nixos/modules/services/mail/mailman.nix b/nixos/modules/services/mail/mailman.nix
index 0ca87696b14..ec2a19f58bb 100644
--- a/nixos/modules/services/mail/mailman.nix
+++ b/nixos/modules/services/mail/mailman.nix
@@ -44,11 +44,9 @@ let
     transport_file_type: hash
   '';
 
-  mailmanCfg = lib.generators.toINI {}
-    (recursiveUpdate cfg.settings
-      ((optionalAttrs (cfg.restApiPassFile != null) {
-        webservice.admin_pass = "#NIXOS_MAILMAN_REST_API_PASS_SECRET#";
-      })));
+  mailmanCfg = lib.generators.toINI {} (recursiveUpdate cfg.settings {
+    webservice.admin_pass = "#NIXOS_MAILMAN_REST_API_PASS_SECRET#";
+  });
 
   mailmanCfgFile = pkgs.writeText "mailman-raw.cfg" mailmanCfg;
 
@@ -388,6 +386,7 @@ in {
 
     environment.etc."mailman3/settings.py".text = ''
       import os
+      from configparser import ConfigParser
 
       # Required by mailman_web.settings, but will be overridden when
       # settings_local.json is loaded.
@@ -404,10 +403,10 @@ in {
       with open('/var/lib/mailman-web/settings_local.json') as f:
           globals().update(json.load(f))
 
-      ${optionalString (cfg.restApiPassFile != null) ''
-        with open('${cfg.restApiPassFile}') as f:
-            MAILMAN_REST_API_PASS = f.read().rstrip('\n')
-      ''}
+      with open('/etc/mailman.cfg') as f:
+          config = ConfigParser()
+          config.read_file(f)
+          MAILMAN_REST_API_PASS = config['webservice']['admin_pass']
 
       ${optionalString (cfg.ldap.enable) ''
         import ldap
@@ -504,10 +503,14 @@ in {
         path = with pkgs; [ jq ];
         after = optional withPostgresql "postgresql.service";
         requires = optional withPostgresql "postgresql.service";
+        serviceConfig.RemainAfterExit = true;
         serviceConfig.Type = "oneshot";
         script = ''
           install -m0750 -o mailman -g mailman ${mailmanCfgFile} /etc/mailman.cfg
-          ${optionalString (cfg.restApiPassFile != null) ''
+          ${if cfg.restApiPassFile == null then ''
+            sed -i "s/#NIXOS_MAILMAN_REST_API_PASS_SECRET#/$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)/g" \
+              /etc/mailman.cfg
+          '' else ''
             ${pkgs.replace-secret}/bin/replace-secret \
               '#NIXOS_MAILMAN_REST_API_PASS_SECRET#' \
               ${cfg.restApiPassFile} \
@@ -570,10 +573,14 @@ in {
           type = "normal";
           plugins = ["python3"];
           home = webEnv;
-          manage-script-name = true;
-          mount = "${cfg.serve.virtualRoot}=mailman_web.wsgi:application";
           http = "127.0.0.1:18507";
-        };
+        }
+        // (if cfg.serve.virtualRoot == "/"
+          then { module = "mailman_web.wsgi:application"; }
+          else {
+            mount = "${cfg.serve.virtualRoot}=mailman_web.wsgi:application";
+            manage-script-name = true;
+          });
         uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
       in {
         wantedBy = ["multi-user.target"];
@@ -638,7 +645,7 @@ in {
 
   meta = {
     maintainers = with lib.maintainers; [ lheckemann qyliss ma27 ];
-    doc = ./mailman.xml;
+    doc = ./mailman.md;
   };
 
 }
diff --git a/nixos/modules/services/mail/mailman.xml b/nixos/modules/services/mail/mailman.xml
deleted file mode 100644
index 27247fb064f..00000000000
--- a/nixos/modules/services/mail/mailman.xml
+++ /dev/null
@@ -1,94 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-mailman">
-  <title>Mailman</title>
-  <para>
-    <link xlink:href="https://www.list.org">Mailman</link> is free
-    software for managing electronic mail discussion and e-newsletter
-    lists. Mailman and its web interface can be configured using the
-    corresponding NixOS module. Note that this service is best used with
-    an existing, securely configured Postfix setup, as it does not automatically configure this.
-  </para>
-
-  <section xml:id="module-services-mailman-basic-usage">
-    <title>Basic usage with Postfix</title>
-    <para>
-      For a basic configuration with Postfix as the MTA, the following settings are suggested:
-      <programlisting>{ config, ... }: {
-  services.postfix = {
-    enable = true;
-    relayDomains = ["hash:/var/lib/mailman/data/postfix_domains"];
-    sslCert = config.security.acme.certs."lists.example.org".directory + "/full.pem";
-    sslKey = config.security.acme.certs."lists.example.org".directory + "/key.pem";
-    config = {
-      transport_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"];
-      local_recipient_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"];
-    };
-  };
-  services.mailman = {
-    <link linkend="opt-services.mailman.enable">enable</link> = true;
-    <link linkend="opt-services.mailman.serve.enable">serve.enable</link> = true;
-    <link linkend="opt-services.mailman.hyperkitty.enable">hyperkitty.enable</link> = true;
-    <link linkend="opt-services.mailman.webHosts">webHosts</link> = ["lists.example.org"];
-    <link linkend="opt-services.mailman.siteOwner">siteOwner</link> = "mailman@example.org";
-  };
-  <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">services.nginx.virtualHosts."lists.example.org".enableACME</link> = true;
-  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 25 80 443 ];
-}</programlisting>
-    </para>
-    <para>
-      DNS records will also be required:
-      <itemizedlist>
-        <listitem><para><literal>AAAA</literal> and <literal>A</literal> records pointing to the host in question, in order for browsers to be able to discover the address of the web server;</para></listitem>
-        <listitem><para>An <literal>MX</literal> record pointing to a domain name at which the host is reachable, in order for other mail servers to be able to deliver emails to the mailing lists it hosts.</para></listitem>
-      </itemizedlist>
-    </para>
-    <para>
-      After this has been done and appropriate DNS records have been
-      set up, the Postorius mailing list manager and the Hyperkitty
-      archive browser will be available at
-      https://lists.example.org/. Note that this setup is not
-      sufficient to deliver emails to most email providers nor to
-      avoid spam -- a number of additional measures for authenticating
-      incoming and outgoing mails, such as SPF, DMARC and DKIM are
-      necessary, but outside the scope of the Mailman module.
-    </para>
-  </section>
-  <section xml:id="module-services-mailman-other-mtas">
-    <title>Using with other MTAs</title>
-    <para>
-      Mailman also supports other MTA, though with a little bit more configuration. For example, to use Mailman with Exim, you can use the following settings:
-      <programlisting>{ config, ... }: {
-  services = {
-    mailman = {
-      enable = true;
-      siteOwner = "mailman@example.org";
-      <link linkend="opt-services.mailman.enablePostfix">enablePostfix</link> = false;
-      settings.mta = {
-        incoming = "mailman.mta.exim4.LMTP";
-        outgoing = "mailman.mta.deliver.deliver";
-        lmtp_host = "localhost";
-        lmtp_port = "8024";
-        smtp_host = "localhost";
-        smtp_port = "25";
-        configuration = "python:mailman.config.exim4";
-      };
-    };
-    exim = {
-      enable = true;
-      # You can configure Exim in a separate file to reduce configuration.nix clutter
-      config = builtins.readFile ./exim.conf;
-    };
-  };
-}</programlisting>
-    </para>
-    <para>
-      The exim config needs some special additions to work with Mailman. Currently
-      NixOS can't manage Exim config with such granularity. Please refer to
-      <link xlink:href="https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html">Mailman documentation</link>
-      for more info on configuring Mailman for working with Exim.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index d01734d61e8..23c47aaca7e 100644
--- a/nixos/modules/services/mail/postfix.nix
+++ b/nixos/modules/services/mail/postfix.nix
@@ -234,7 +234,7 @@ let
 
   headerChecks = concatStringsSep "\n" (map (x: "${x.pattern} ${x.action}") cfg.headerChecks) + cfg.extraHeaderChecks;
 
-  aliases = let separator = if cfg.aliasMapType == "hash" then ":" else ""; in
+  aliases = let separator = optionalString (cfg.aliasMapType == "hash") ":"; in
     optionalString (cfg.postmasterAlias != "") ''
       postmaster${separator} ${cfg.postmasterAlias}
     ''
@@ -809,7 +809,7 @@ in
       // optionalAttrs (cfg.relayHost != "") { relayhost = if cfg.lookupMX
                                                            then "${cfg.relayHost}:${toString cfg.relayPort}"
                                                            else "[${cfg.relayHost}]:${toString cfg.relayPort}"; }
-      // optionalAttrs config.networking.enableIPv6 { inet_protocols = mkDefault "all"; }
+      // optionalAttrs (!config.networking.enableIPv6) { inet_protocols = mkDefault "ipv4"; }
       // optionalAttrs (cfg.networks != null) { mynetworks = cfg.networks; }
       // optionalAttrs (cfg.networksStyle != "") { mynetworks_style = cfg.networksStyle; }
       // optionalAttrs (cfg.hostname != "") { myhostname = cfg.hostname; }
diff --git a/nixos/modules/services/mail/public-inbox.nix b/nixos/modules/services/mail/public-inbox.nix
index 9cd6726e6cb..4944d46fbd7 100644
--- a/nixos/modules/services/mail/public-inbox.nix
+++ b/nixos/modules/services/mail/public-inbox.nix
@@ -89,7 +89,7 @@ let
       PrivateNetwork = mkDefault (!needNetwork);
       ProcSubset = "pid";
       ProtectClock = true;
-      ProtectHome = mkDefault true;
+      ProtectHome = "tmpfs";
       ProtectHostname = true;
       ProtectKernelLogs = true;
       ProtectProc = "invisible";
@@ -177,8 +177,7 @@ in
           description = lib.mdDoc "The email addresses of the public-inbox.";
         };
         options.url = mkOption {
-          type = with types; nullOr str;
-          default = null;
+          type = types.nonEmptyStr;
           example = "https://example.org/lists/example-discuss";
           description = lib.mdDoc "URL where this inbox can be accessed over HTTP.";
         };
@@ -275,9 +274,8 @@ in
           default = {};
           description = lib.mdDoc "public inboxes";
           type = types.submodule {
-            # Keeping in line with the tradition of unnecessarily specific types, allow users to set
-            # freeform settings either globally under the `publicinbox` section, or for specific
-            # inboxes through additional nesting.
+            # Support both global options like `services.public-inbox.settings.publicinbox.imapserver`
+            # and inbox specific options like `services.public-inbox.settings.publicinbox.foo.address`.
             freeformType = with types; attrsOf (oneOf [ iniAtom (attrsOf iniAtom) ]);
 
             options.css = mkOption {
@@ -285,12 +283,24 @@ in
               default = [];
               description = lib.mdDoc "The local path name of a CSS file for the PSGI web interface.";
             };
+            options.imapserver = mkOption {
+              type = with types; listOf str;
+              default = [];
+              example = [ "imap.public-inbox.org" ];
+              description = lib.mdDoc "IMAP URLs to this public-inbox instance";
+            };
             options.nntpserver = mkOption {
               type = with types; listOf str;
               default = [];
               example = [ "nntp://news.public-inbox.org" "nntps://news.public-inbox.org" ];
               description = lib.mdDoc "NNTP URLs to this public-inbox instance";
             };
+            options.pop3server = mkOption {
+              type = with types; listOf str;
+              default = [];
+              example = [ "pop.public-inbox.org" ];
+              description = lib.mdDoc "POP3 URLs to this public-inbox instance";
+            };
             options.wwwlisting = mkOption {
               type = with types; enum [ "all" "404" "match=domain" ];
               default = "404";
@@ -450,6 +460,8 @@ in
           after = [ "public-inbox-init.service" "public-inbox-watch.service" ];
           requires = [ "public-inbox-init.service" ];
           serviceConfig = {
+            BindPathsReadOnly =
+              map (c: c.dir) (lib.attrValues cfg.settings.coderepo);
             ExecStart = escapeShellArgs (
               [ "${cfg.package}/bin/public-inbox-httpd" ] ++
               cfg.http.args ++
@@ -553,16 +565,7 @@ in
                 ${pkgs.git}/bin/git config core.sharedRepository 0640
               fi
             '') cfg.inboxes
-            ) + ''
-            shopt -s nullglob
-            for inbox in ${stateDir}/inboxes/*/; do
-              # This should be idempotent, but only do it for new
-              # inboxes anyway because it's only needed once, and could
-              # be slow for large pre-existing inboxes.
-              ls -1 "$inbox" | grep -q '^xap' ||
-              ${cfg.package}/bin/public-inbox-index "$inbox"
-            done
-          '';
+            );
           serviceConfig = {
             Type = "oneshot";
             RemainAfterExit = true;
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index e05820fb87c..22a4e3c451a 100644
--- a/nixos/modules/services/mail/roundcube.nix
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -7,7 +7,7 @@ let
   fpm = config.services.phpfpm.pools.roundcube;
   localDB = cfg.database.host == "localhost";
   user = cfg.database.username;
-  phpWithPspell = pkgs.php80.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled);
+  phpWithPspell = pkgs.php81.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled);
 in
 {
   options.services.roundcube = {
@@ -70,7 +70,12 @@ in
       };
       passwordFile = mkOption {
         type = types.str;
-        description = lib.mdDoc "Password file for the postgresql connection. Must be readable by user `nginx`. Ignored if `database.host` is set to `localhost`, as peer authentication will be used.";
+        description = lib.mdDoc ''
+          Password file for the postgresql connection.
+          Must be formatted according to PostgreSQL .pgpass standard (see https://www.postgresql.org/docs/current/libpq-pgpass.html)
+          but only one line, no comments and readable by user `nginx`.
+          Ignored if `database.host` is set to `localhost`, as peer authentication will be used.
+        '';
       };
       dbname = mkOption {
         type = types.str;
@@ -123,7 +128,13 @@ in
     environment.etc."roundcube/config.inc.php".text = ''
       <?php
 
-      ${lib.optionalString (!localDB) "$password = file_get_contents('${cfg.database.passwordFile}');"}
+      ${lib.optionalString (!localDB) ''
+        $password = file('${cfg.database.passwordFile}')[0];
+        $password = preg_split('~\\\\.(*SKIP)(*FAIL)|\:~s', $password);
+        $password = end($password);
+        $password = str_replace("\\:", ":", $password);
+        $password = str_replace("\\\\", "\\", $password);
+      ''}
 
       $config = array();
       $config['db_dsnw'] = 'pgsql://${cfg.database.username}${lib.optionalString (!localDB) ":' . $password . '"}@${if localDB then "unix(/run/postgresql)" else cfg.database.host}/${cfg.database.dbname}';
@@ -132,6 +143,8 @@ in
       $config['plugins'] = [${concatMapStringsSep "," (p: "'${p}'") cfg.plugins}];
       $config['des_key'] = file_get_contents('/var/lib/roundcube/des_key');
       $config['mime_types'] = '${pkgs.nginx}/conf/mime.types';
+      # Roundcube uses PHP-FPM which has `PrivateTmp = true;`
+      $config['temp_dir'] = '/tmp';
       $config['enable_spellcheck'] = ${if cfg.dicts == [] then "false" else "true"};
       # by default, spellchecking uses a third-party cloud services
       $config['spellcheck_engine'] = 'pspell';
@@ -150,9 +163,13 @@ in
             root = cfg.package;
             index = "index.php";
             extraConfig = ''
-              location ~* \.php$ {
+              location ~* \.php(/|$) {
                 fastcgi_split_path_info ^(.+\.php)(/.+)$;
                 fastcgi_pass unix:${fpm.socket};
+
+                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+                fastcgi_param PATH_INFO       $fastcgi_path_info;
+
                 include ${config.services.nginx.package}/conf/fastcgi_params;
                 include ${pkgs.nginx}/conf/fastcgi.conf;
               }
@@ -217,6 +234,7 @@ in
         path = [ config.services.postgresql.package ];
       })
       {
+        after = [ "network-online.target" ];
         wantedBy = [ "multi-user.target" ];
         script = let
           psql = "${lib.optionalString (!localDB) "PGPASSFILE=${cfg.database.passwordFile}"} ${pkgs.postgresql}/bin/psql ${lib.optionalString (!localDB) "-h ${cfg.database.host} -U ${cfg.database.username} "} ${cfg.database.dbname}";
diff --git a/nixos/modules/services/mail/rspamd.nix b/nixos/modules/services/mail/rspamd.nix
index f9be9024dd4..ca88d812217 100644
--- a/nixos/modules/services/mail/rspamd.nix
+++ b/nixos/modules/services/mail/rspamd.nix
@@ -215,7 +215,7 @@ let
       text = v.extraConfig;
     })
     (filterAttrs (n: v: v.extraConfig != "") cfg.workers))
-    // (if cfg.extraConfig == "" then {} else {
+    // (lib.optionalAttrs (cfg.extraConfig != "") {
       "extra-config.inc".text = cfg.extraConfig;
     });
 in
diff --git a/nixos/modules/services/mail/spamassassin.nix b/nixos/modules/services/mail/spamassassin.nix
index 49d1d931598..072172e3145 100644
--- a/nixos/modules/services/mail/spamassassin.nix
+++ b/nixos/modules/services/mail/spamassassin.nix
@@ -77,9 +77,9 @@ in
           loadplugin Mail::SpamAssassin::Plugin::Check
           #loadplugin Mail::SpamAssassin::Plugin::DCC
           loadplugin Mail::SpamAssassin::Plugin::DKIM
+          loadplugin Mail::SpamAssassin::Plugin::DMARC
           loadplugin Mail::SpamAssassin::Plugin::DNSEval
           loadplugin Mail::SpamAssassin::Plugin::FreeMail
-          loadplugin Mail::SpamAssassin::Plugin::Hashcash
           loadplugin Mail::SpamAssassin::Plugin::HeaderEval
           loadplugin Mail::SpamAssassin::Plugin::HTMLEval
           loadplugin Mail::SpamAssassin::Plugin::HTTPSMismatch
diff --git a/nixos/modules/services/mail/zeyple.nix b/nixos/modules/services/mail/zeyple.nix
new file mode 100644
index 00000000000..e7f9ddd92dc
--- /dev/null
+++ b/nixos/modules/services/mail/zeyple.nix
@@ -0,0 +1,125 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.zeyple;
+  ini = pkgs.formats.ini { };
+
+  gpgHome = pkgs.runCommand "zeyple-gpg-home" { } ''
+    mkdir -p $out
+    for file in ${lib.concatStringsSep " " cfg.keys}; do
+      ${config.programs.gnupg.package}/bin/gpg --homedir="$out" --import "$file"
+    done
+
+    # Remove socket files
+    rm -f $out/S.*
+  '';
+in {
+  options.services.zeyple = {
+    enable = mkEnableOption (lib.mdDoc "Zeyple, an utility program to automatically encrypt outgoing emails with GPG");
+
+    user = mkOption {
+      type = types.str;
+      default = "zeyple";
+      description = lib.mdDoc ''
+        User to run Zeyple as.
+
+        ::: {.note}
+        If left as the default value this user will automatically be created
+        on system activation, otherwise the sysadmin is responsible for
+        ensuring the user exists.
+        :::
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "zeyple";
+      description = lib.mdDoc ''
+        Group to use to run Zeyple.
+
+        ::: {.note}
+        If left as the default value this group will automatically be created
+        on system activation, otherwise the sysadmin is responsible for
+        ensuring the user exists.
+        :::
+      '';
+    };
+
+    settings = mkOption {
+      type = ini.type;
+      default = { };
+      description = lib.mdDoc ''
+        Zeyple configuration. refer to
+        <https://github.com/infertux/zeyple/blob/master/zeyple/zeyple.conf.example>
+        for details on supported values.
+      '';
+    };
+
+    keys = mkOption {
+      type = with types; listOf path;
+      description = lib.mdDoc "List of public key files that will be imported by gpg.";
+    };
+
+    rotateLogs = mkOption {
+      type = types.bool;
+      default = true;
+      description = lib.mdDoc "Whether to enable rotation of log files.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups = optionalAttrs (cfg.group == "zeyple") { "${cfg.group}" = { }; };
+    users.users = optionalAttrs (cfg.user == "zeyple") {
+      "${cfg.user}" = {
+        isSystemUser = true;
+        group = cfg.group;
+      };
+    };
+
+    services.zeyple.settings = {
+      zeyple = mapAttrs (name: mkDefault) {
+        log_file = "/var/log/zeyple/zeyple.log";
+        force_encrypt = true;
+      };
+
+      gpg = mapAttrs (name: mkDefault) { home = "${gpgHome}"; };
+
+      relay = mapAttrs (name: mkDefault) {
+        host = "localhost";
+        port = 10026;
+      };
+    };
+
+    environment.etc."zeyple.conf".source = ini.generate "zeyple.conf" cfg.settings;
+
+    systemd.tmpfiles.rules = [ "f '${cfg.settings.zeyple.log_file}' 0600 ${cfg.user} ${cfg.group} - -" ];
+    services.logrotate = mkIf cfg.rotateLogs {
+      enable = true;
+      settings.zeyple = {
+        files = cfg.settings.zeyple.log_file;
+        frequency = "weekly";
+        rotate = 5;
+        compress = true;
+        copytruncate = true;
+      };
+    };
+
+    services.postfix.extraMasterConf = ''
+      zeyple    unix  -       n       n       -       -       pipe
+        user=${cfg.user} argv=${pkgs.zeyple}/bin/zeyple ''${recipient}
+
+      localhost:${toString cfg.settings.relay.port} inet  n       -       n       -       10      smtpd
+        -o content_filter=
+        -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_milters
+        -o smtpd_helo_restrictions=
+        -o smtpd_client_restrictions=
+        -o smtpd_sender_restrictions=
+        -o smtpd_recipient_restrictions=permit_mynetworks,reject
+        -o mynetworks=127.0.0.0/8,[::1]/128
+        -o smtpd_authorized_xforward_hosts=127.0.0.0/8,[::1]/128
+    '';
+
+    services.postfix.extraConfig = "content_filter = zeyple";
+  };
+}
diff --git a/nixos/modules/services/matrix/appservice-discord.nix b/nixos/modules/services/matrix/appservice-discord.nix
index 15f0f0cc0cd..f579c2529c0 100644
--- a/nixos/modules/services/matrix/appservice-discord.nix
+++ b/nixos/modules/services/matrix/appservice-discord.nix
@@ -5,7 +5,6 @@ with lib;
 let
   dataDir = "/var/lib/matrix-appservice-discord";
   registrationFile = "${dataDir}/discord-registration.yaml";
-  appDir = "${pkgs.matrix-appservice-discord}/${pkgs.matrix-appservice-discord.passthru.nodeAppDir}";
   cfg = config.services.matrix-appservice-discord;
   opt = options.services.matrix-appservice-discord;
   # TODO: switch to configGen.json once RFC42 is implemented
@@ -16,6 +15,15 @@ in {
     services.matrix-appservice-discord = {
       enable = mkEnableOption (lib.mdDoc "a bridge between Matrix and Discord");
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.matrix-appservice-discord;
+        defaultText = literalExpression "pkgs.matrix-appservice-discord";
+        description = lib.mdDoc ''
+          Which package of matrix-appservice-discord to use.
+        '';
+      };
+
       settings = mkOption rec {
         # TODO: switch to types.config.json as prescribed by RFC42 once it's implemented
         type = types.attrs;
@@ -114,7 +122,7 @@ in {
 
       preStart = ''
         if [ ! -f '${registrationFile}' ]; then
-          ${pkgs.matrix-appservice-discord}/bin/matrix-appservice-discord \
+          ${cfg.package}/bin/matrix-appservice-discord \
             --generate-registration \
             --url=${escapeShellArg cfg.url} \
             ${optionalString (cfg.localpart != null) "--localpart=${escapeShellArg cfg.localpart}"} \
@@ -135,13 +143,13 @@ in {
 
         DynamicUser = true;
         PrivateTmp = true;
-        WorkingDirectory = appDir;
+        WorkingDirectory = "${cfg.package}/${cfg.package.passthru.nodeAppDir}";
         StateDirectory = baseNameOf dataDir;
         UMask = "0027";
         EnvironmentFile = cfg.environmentFile;
 
         ExecStart = ''
-          ${pkgs.matrix-appservice-discord}/bin/matrix-appservice-discord \
+          ${cfg.package}/bin/matrix-appservice-discord \
             --file='${registrationFile}' \
             --config='${settingsFile}' \
             --port='${toString cfg.port}'
diff --git a/nixos/modules/services/matrix/appservice-irc.nix b/nixos/modules/services/matrix/appservice-irc.nix
index 388553d4182..5526df785c3 100644
--- a/nixos/modules/services/matrix/appservice-irc.nix
+++ b/nixos/modules/services/matrix/appservice-irc.nix
@@ -187,7 +187,7 @@ in {
           sed -i "s/^as_token:.*$/$as_token/g" ${registrationFile}
         fi
         # Allow synapse access to the registration
-        if ${getBin pkgs.glibc}/bin/getent group matrix-synapse > /dev/null; then
+        if ${pkgs.getent}/bin/getent group matrix-synapse > /dev/null; then
           chgrp matrix-synapse ${registrationFile}
           chmod g+r ${registrationFile}
         fi
diff --git a/nixos/modules/services/matrix/dendrite.nix b/nixos/modules/services/matrix/dendrite.nix
index a5fea3da484..244c15fbf7a 100644
--- a/nixos/modules/services/matrix/dendrite.nix
+++ b/nixos/modules/services/matrix/dendrite.nix
@@ -159,6 +159,15 @@ in
             '';
           };
         };
+        options.relay_api.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:relayapi.db";
+            description = lib.mdDoc ''
+              Database for the Relay Server.
+            '';
+          };
+        };
         options.media_api = {
           database = {
             connection_string = lib.mkOption {
@@ -288,13 +297,13 @@ in
         LimitNOFILE = 65535;
         EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
         LoadCredential = cfg.loadCredential;
-        ExecStartPre = ''
+        ExecStartPre = [''
           ${pkgs.envsubst}/bin/envsubst \
             -i ${configurationYaml} \
             -o /run/dendrite/dendrite.yaml
-        '';
+        ''];
         ExecStart = lib.strings.concatStringsSep " " ([
-          "${pkgs.dendrite}/bin/dendrite-monolith-server"
+          "${pkgs.dendrite}/bin/dendrite"
           "--config /run/dendrite/dendrite.yaml"
         ] ++ lib.optionals (cfg.httpPort != null) [
           "--http-bind-address :${builtins.toString cfg.httpPort}"
diff --git a/nixos/modules/services/matrix/mautrix-facebook.nix b/nixos/modules/services/matrix/mautrix-facebook.nix
index e74f25df764..bab6865496d 100644
--- a/nixos/modules/services/matrix/mautrix-facebook.nix
+++ b/nixos/modules/services/matrix/mautrix-facebook.nix
@@ -29,6 +29,7 @@ in {
           };
 
           appservice = rec {
+            id = "facebook";
             address = "http://${hostname}:${toString port}";
             hostname = "localhost";
             port = 29319;
@@ -96,7 +97,7 @@ in {
         type = types.nullOr types.path;
         default = null;
         description = lib.mdDoc ''
-          File containing environment variables to be passed to the mautrix-telegram service.
+          File containing environment variables to be passed to the mautrix-facebook service.
 
           Any config variable can be overridden by setting `MAUTRIX_FACEBOOK_SOME_KEY` to override the `some.key` variable.
         '';
@@ -171,7 +172,7 @@ in {
 
     services.mautrix-facebook = {
       registrationData = {
-        id = "mautrix-facebook";
+        id = cfg.settings.appservice.id;
 
         namespaces = {
           users = [
diff --git a/nixos/modules/services/matrix/mautrix-telegram.nix b/nixos/modules/services/matrix/mautrix-telegram.nix
index 5a632fd27e8..17032ed808e 100644
--- a/nixos/modules/services/matrix/mautrix-telegram.nix
+++ b/nixos/modules/services/matrix/mautrix-telegram.nix
@@ -80,6 +80,9 @@ in {
               "example.com" = "full";
               "@admin:example.com" = "admin";
             };
+            telegram = {
+              connection.use_ipv6 = true;
+            };
           }
         '';
         description = lib.mdDoc ''
@@ -137,7 +140,7 @@ in {
       wantedBy = [ "multi-user.target" ];
       wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
       after = [ "network-online.target" ] ++ cfg.serviceDependencies;
-      path = [ pkgs.lottieconverter ];
+      path = [ pkgs.lottieconverter pkgs.ffmpeg-full ];
 
       # mautrix-telegram tries to generate a dotfile in the home directory of
       # the running user if using a postgresql database:
diff --git a/nixos/modules/services/matrix/mjolnir.md b/nixos/modules/services/matrix/mjolnir.md
new file mode 100644
index 00000000000..f6994eeb8fa
--- /dev/null
+++ b/nixos/modules/services/matrix/mjolnir.md
@@ -0,0 +1,110 @@
+# Mjolnir (Matrix Moderation Tool) {#module-services-mjolnir}
+
+This chapter will show you how to set up your own, self-hosted
+[Mjolnir](https://github.com/matrix-org/mjolnir) instance.
+
+As an all-in-one moderation tool, it can protect your server from
+malicious invites, spam messages, and whatever else you don't want.
+In addition to server-level protection, Mjolnir is great for communities
+wanting to protect their rooms without having to use their personal
+accounts for moderation.
+
+The bot by default includes support for bans, redactions, anti-spam,
+server ACLs, room directory changes, room alias transfers, account
+deactivation, room shutdown, and more.
+
+See the [README](https://github.com/matrix-org/mjolnir#readme)
+page and the [Moderator's guide](https://github.com/matrix-org/mjolnir/blob/main/docs/moderators.md)
+for additional instructions on how to setup and use Mjolnir.
+
+For [additional settings](#opt-services.mjolnir.settings)
+see [the default configuration](https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml).
+
+## Mjolnir Setup {#module-services-mjolnir-setup}
+
+First create a new Room which will be used as a management room for Mjolnir. In
+this room, Mjolnir will log possible errors and debugging information. You'll
+need to set this Room-ID in [services.mjolnir.managementRoom](#opt-services.mjolnir.managementRoom).
+
+Next, create a new user for Mjolnir on your homeserver, if not present already.
+
+The Mjolnir Matrix user expects to be free of any rate limiting.
+See [Synapse #6286](https://github.com/matrix-org/synapse/issues/6286)
+for an example on how to achieve this.
+
+If you want Mjolnir to be able to deactivate users, move room aliases, shutdown rooms, etc.
+you'll need to make the Mjolnir user a Matrix server admin.
+
+Now invite the Mjolnir user to the management room.
+
+It is recommended to use [Pantalaimon](https://github.com/matrix-org/pantalaimon),
+so your management room can be encrypted. This also applies if you are looking to moderate an encrypted room.
+
+To enable the Pantalaimon E2E Proxy for mjolnir, enable
+[services.mjolnir.pantalaimon](#opt-services.mjolnir.pantalaimon.enable). This will
+autoconfigure a new Pantalaimon instance, which will connect to the homeserver
+set in [services.mjolnir.homeserverUrl](#opt-services.mjolnir.homeserverUrl) and Mjolnir itself
+will be configured to connect to the new Pantalaimon instance.
+
+```
+{
+  services.mjolnir = {
+    enable = true;
+    homeserverUrl = "https://matrix.domain.tld";
+    pantalaimon = {
+       enable = true;
+       username = "mjolnir";
+       passwordFile = "/run/secrets/mjolnir-password";
+    };
+    protectedRooms = [
+      "https://matrix.to/#/!xxx:domain.tld"
+    ];
+    managementRoom = "!yyy:domain.tld";
+  };
+}
+```
+
+### Element Matrix Services (EMS) {#module-services-mjolnir-setup-ems}
+
+If you are using a managed ["Element Matrix Services (EMS)"](https://ems.element.io/)
+server, you will need to consent to the terms and conditions. Upon startup, an error
+log entry with a URL to the consent page will be generated.
+
+## Synapse Antispam Module {#module-services-mjolnir-matrix-synapse-antispam}
+
+A Synapse module is also available to apply the same rulesets the bot
+uses across an entire homeserver.
+
+To use the Antispam Module, add `matrix-synapse-plugins.matrix-synapse-mjolnir-antispam`
+to the Synapse plugin list and enable the `mjolnir.Module` module.
+
+```
+{
+  services.matrix-synapse = {
+    plugins = with pkgs; [
+      matrix-synapse-plugins.matrix-synapse-mjolnir-antispam
+    ];
+    extraConfig = ''
+      modules:
+        - module: mjolnir.Module
+          config:
+            # Prevent servers/users in the ban lists from inviting users on this
+            # server to rooms. Default true.
+            block_invites: true
+            # Flag messages sent by servers/users in the ban lists as spam. Currently
+            # this means that spammy messages will appear as empty to users. Default
+            # false.
+            block_messages: false
+            # Remove users from the user directory search by filtering matrix IDs and
+            # display names by the entries in the user ban list. Default false.
+            block_usernames: false
+            # The room IDs of the ban lists to honour. Unlike other parts of Mjolnir,
+            # this list cannot be room aliases or permalinks. This server is expected
+            # to already be joined to the room - Mjolnir will not automatically join
+            # these rooms.
+            ban_lists:
+              - "!roomid:example.org"
+    '';
+  };
+}
+```
diff --git a/nixos/modules/services/matrix/mjolnir.nix b/nixos/modules/services/matrix/mjolnir.nix
index cbf7b93329d..0824be66334 100644
--- a/nixos/modules/services/matrix/mjolnir.nix
+++ b/nixos/modules/services/matrix/mjolnir.nix
@@ -30,7 +30,7 @@ let
 
   # these config files will be merged one after the other to build the final config
   configFiles = [
-    "${pkgs.mjolnir}/share/mjolnir/config/default.yaml"
+    "${pkgs.mjolnir}/libexec/mjolnir/deps/mjolnir/config/default.yaml"
     moduleConfigFile
   ];
 
@@ -200,7 +200,7 @@ in
       wantedBy = [ "multi-user.target" ];
 
       serviceConfig = {
-        ExecStart = ''${pkgs.mjolnir}/bin/mjolnir'';
+        ExecStart = ''${pkgs.mjolnir}/bin/mjolnir --mjolnir-config ./config/default.yaml'';
         ExecStartPre = [ generateConfig ];
         WorkingDirectory = cfg.dataPath;
         StateDirectory = "mjolnir";
@@ -236,7 +236,7 @@ in
   };
 
   meta = {
-    doc = ./mjolnir.xml;
+    doc = ./mjolnir.md;
     maintainers = with maintainers; [ jojosch ];
   };
 }
diff --git a/nixos/modules/services/matrix/mjolnir.xml b/nixos/modules/services/matrix/mjolnir.xml
deleted file mode 100644
index b07abe33979..00000000000
--- a/nixos/modules/services/matrix/mjolnir.xml
+++ /dev/null
@@ -1,134 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-mjolnir">
- <title>Mjolnir (Matrix Moderation Tool)</title>
- <para>
-  This chapter will show you how to set up your own, self-hosted
-  <link xlink:href="https://github.com/matrix-org/mjolnir">Mjolnir</link>
-  instance.
- </para>
- <para>
-  As an all-in-one moderation tool, it can protect your server from
-  malicious invites, spam messages, and whatever else you don't want.
-  In addition to server-level protection, Mjolnir is great for communities
-  wanting to protect their rooms without having to use their personal
-  accounts for moderation.
- </para>
- <para>
-  The bot by default includes support for bans, redactions, anti-spam,
-  server ACLs, room directory changes, room alias transfers, account
-  deactivation, room shutdown, and more.
- </para>
- <para>
-  See the <link xlink:href="https://github.com/matrix-org/mjolnir#readme">README</link>
-  page and the <link xlink:href="https://github.com/matrix-org/mjolnir/blob/main/docs/moderators.md">Moderator's guide</link>
-  for additional instructions on how to setup and use Mjolnir.
- </para>
- <para>
-  For <link linkend="opt-services.mjolnir.settings">additional settings</link>
-  see <link xlink:href="https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml">the default configuration</link>.
- </para>
- <section xml:id="module-services-mjolnir-setup">
-  <title>Mjolnir Setup</title>
-  <para>
-   First create a new Room which will be used as a management room for Mjolnir. In
-   this room, Mjolnir will log possible errors and debugging information. You'll
-   need to set this Room-ID in <link linkend="opt-services.mjolnir.managementRoom">services.mjolnir.managementRoom</link>.
-  </para>
-  <para>
-   Next, create a new user for Mjolnir on your homeserver, if not present already.
-  </para>
-  <para>
-   The Mjolnir Matrix user expects to be free of any rate limiting.
-   See <link xlink:href="https://github.com/matrix-org/synapse/issues/6286">Synapse #6286</link>
-   for an example on how to achieve this.
-  </para>
-  <para>
-   If you want Mjolnir to be able to deactivate users, move room aliases, shutdown rooms, etc.
-   you'll need to make the Mjolnir user a Matrix server admin.
-  </para>
-  <para>
-   Now invite the Mjolnir user to the management room.
-  </para>
-  <para>
-   It is recommended to use <link xlink:href="https://github.com/matrix-org/pantalaimon">Pantalaimon</link>,
-   so your management room can be encrypted. This also applies if you are looking to moderate an encrypted room.
-  </para>
-  <para>
-   To enable the Pantalaimon E2E Proxy for mjolnir, enable
-   <link linkend="opt-services.mjolnir.pantalaimon.enable">services.mjolnir.pantalaimon</link>. This will
-   autoconfigure a new Pantalaimon instance, which will connect to the homeserver
-   set in <link linkend="opt-services.mjolnir.homeserverUrl">services.mjolnir.homeserverUrl</link> and Mjolnir itself
-   will be configured to connect to the new Pantalaimon instance.
-  </para>
-<programlisting>
-{
-  services.mjolnir = {
-    enable = true;
-    <link linkend="opt-services.mjolnir.homeserverUrl">homeserverUrl</link> = "https://matrix.domain.tld";
-    <link linkend="opt-services.mjolnir.pantalaimon">pantalaimon</link> = {
-       <link linkend="opt-services.mjolnir.pantalaimon.enable">enable</link> = true;
-       <link linkend="opt-services.mjolnir.pantalaimon.username">username</link> = "mjolnir";
-       <link linkend="opt-services.mjolnir.pantalaimon.passwordFile">passwordFile</link> = "/run/secrets/mjolnir-password";
-    };
-    <link linkend="opt-services.mjolnir.protectedRooms">protectedRooms</link> = [
-      "https://matrix.to/#/!xxx:domain.tld"
-    ];
-    <link linkend="opt-services.mjolnir.managementRoom">managementRoom</link> = "!yyy:domain.tld";
-  };
-}
-</programlisting>
- <section xml:id="module-services-mjolnir-setup-ems">
-  <title>Element Matrix Services (EMS)</title>
-  <para>
-   If you are using a managed <link xlink:href="https://ems.element.io/">"Element Matrix Services (EMS)"</link>
-   server, you will need to consent to the terms and conditions. Upon startup, an error
-   log entry with a URL to the consent page will be generated.
-  </para>
- </section>
- </section>
-
- <section xml:id="module-services-mjolnir-matrix-synapse-antispam">
-  <title>Synapse Antispam Module</title>
-  <para>
-   A Synapse module is also available to apply the same rulesets the bot
-   uses across an entire homeserver.
-  </para>
-  <para>
-   To use the Antispam Module, add <package>matrix-synapse-plugins.matrix-synapse-mjolnir-antispam</package>
-   to the Synapse plugin list and enable the <literal>mjolnir.Module</literal> module.
-  </para>
-<programlisting>
-{
-  services.matrix-synapse = {
-    plugins = with pkgs; [
-      matrix-synapse-plugins.matrix-synapse-mjolnir-antispam
-    ];
-    extraConfig = ''
-      modules:
-        - module: mjolnir.Module
-          config:
-            # Prevent servers/users in the ban lists from inviting users on this
-            # server to rooms. Default true.
-            block_invites: true
-            # Flag messages sent by servers/users in the ban lists as spam. Currently
-            # this means that spammy messages will appear as empty to users. Default
-            # false.
-            block_messages: false
-            # Remove users from the user directory search by filtering matrix IDs and
-            # display names by the entries in the user ban list. Default false.
-            block_usernames: false
-            # The room IDs of the ban lists to honour. Unlike other parts of Mjolnir,
-            # this list cannot be room aliases or permalinks. This server is expected
-            # to already be joined to the room - Mjolnir will not automatically join
-            # these rooms.
-            ban_lists:
-              - "!roomid:example.org"
-    '';
-  };
-}
-</programlisting>
- </section>
-</chapter>
diff --git a/nixos/modules/services/misc/mx-puppet-discord.nix b/nixos/modules/services/matrix/mx-puppet-discord.nix
index 36c9f8b122e..36c9f8b122e 100644
--- a/nixos/modules/services/misc/mx-puppet-discord.nix
+++ b/nixos/modules/services/matrix/mx-puppet-discord.nix
diff --git a/nixos/modules/services/matrix/synapse.md b/nixos/modules/services/matrix/synapse.md
new file mode 100644
index 00000000000..cad91ebf58d
--- /dev/null
+++ b/nixos/modules/services/matrix/synapse.md
@@ -0,0 +1,213 @@
+# Matrix {#module-services-matrix}
+
+[Matrix](https://matrix.org/) is an open standard for
+interoperable, decentralised, real-time communication over IP. It can be used
+to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things
+communication - or anywhere you need a standard HTTP API for publishing and
+subscribing to data whilst tracking the conversation history.
+
+This chapter will show you how to set up your own, self-hosted Matrix
+homeserver using the Synapse reference homeserver, and how to serve your own
+copy of the Element web client. See the
+[Try Matrix Now!](https://matrix.org/docs/projects/try-matrix-now.html)
+overview page for links to Element Apps for Android and iOS,
+desktop clients, as well as bridges to other networks and other projects
+around Matrix.
+
+## Synapse Homeserver {#module-services-matrix-synapse}
+
+[Synapse](https://github.com/matrix-org/synapse) is
+the reference homeserver implementation of Matrix from the core development
+team at matrix.org. The following configuration example will set up a
+synapse server for the `example.org` domain, served from
+the host `myhostname.example.org`. For more information,
+please refer to the
+[installation instructions of Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) .
+```
+{ pkgs, lib, config, ... }:
+let
+  fqdn = "${config.networking.hostName}.${config.networking.domain}";
+  clientConfig."m.homeserver".base_url = "https://${fqdn}";
+  serverConfig."m.server" = "${fqdn}:443";
+  mkWellKnown = data: ''
+    add_header Content-Type application/json;
+    add_header Access-Control-Allow-Origin *;
+    return 200 '${builtins.toJSON data}';
+  '';
+in {
+  networking.hostName = "myhostname";
+  networking.domain = "example.org";
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+  services.postgresql.enable = true;
+  services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
+    CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
+    CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
+      TEMPLATE template0
+      LC_COLLATE = "C"
+      LC_CTYPE = "C";
+  '';
+
+  services.nginx = {
+    enable = true;
+    recommendedTlsSettings = true;
+    recommendedOptimisation = true;
+    recommendedGzipSettings = true;
+    recommendedProxySettings = true;
+    virtualHosts = {
+      # If the A and AAAA DNS records on example.org do not point on the same host as the
+      # records for myhostname.example.org, you can easily move the /.well-known
+      # virtualHost section of the code to the host that is serving example.org, while
+      # the rest stays on myhostname.example.org with no other changes required.
+      # This pattern also allows to seamlessly move the homeserver from
+      # myhostname.example.org to myotherhost.example.org by only changing the
+      # /.well-known redirection target.
+      "${config.networking.domain}" = {
+        enableACME = true;
+        forceSSL = true;
+        # This section is not needed if the server_name of matrix-synapse is equal to
+        # the domain (i.e. example.org from @foo:example.org) and the federation port
+        # is 8448.
+        # Further reference can be found in the docs about delegation under
+        # https://matrix-org.github.io/synapse/latest/delegate.html
+        locations."= /.well-known/matrix/server".extraConfig = mkWellKnown serverConfig;
+        # This is usually needed for homeserver discovery (from e.g. other Matrix clients).
+        # Further reference can be found in the upstream docs at
+        # https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient
+        locations."= /.well-known/matrix/client".extraConfig = mkWellKnown clientConfig;
+      };
+      "${fqdn}" = {
+        enableACME = true;
+        forceSSL = true;
+        # It's also possible to do a redirect here or something else, this vhost is not
+        # needed for Matrix. It's recommended though to *not put* element
+        # here, see also the section about Element.
+        locations."/".extraConfig = ''
+          return 404;
+        '';
+        # Forward all Matrix API calls to the synapse Matrix homeserver. A trailing slash
+        # *must not* be used here.
+        locations."/_matrix".proxyPass = "http://[::1]:8008";
+        # Forward requests for e.g. SSO and password-resets.
+        locations."/_synapse/client".proxyPass = "http://[::1]:8008";
+      };
+    };
+  };
+
+  services.matrix-synapse = {
+    enable = true;
+    settings.server_name = config.networking.domain;
+    settings.listeners = [
+      { port = 8008;
+        bind_addresses = [ "::1" ];
+        type = "http";
+        tls = false;
+        x_forwarded = true;
+        resources = [ {
+          names = [ "client" "federation" ];
+          compress = true;
+        } ];
+      }
+    ];
+  };
+}
+```
+
+## Registering Matrix users {#module-services-matrix-register-users}
+
+If you want to run a server with public registration by anybody, you can
+then enable `services.matrix-synapse.settings.enable_registration = true;`.
+Otherwise, or you can generate a registration secret with
+{command}`pwgen -s 64 1` and set it with
+[](#opt-services.matrix-synapse.settings.registration_shared_secret).
+To create a new user or admin, run the following after you have set the secret
+and have rebuilt NixOS:
+```ShellSession
+$ nix-shell -p matrix-synapse
+$ register_new_matrix_user -k your-registration-shared-secret http://localhost:8008
+New user localpart: your-username
+Password:
+Confirm password:
+Make admin [no]:
+Success!
+```
+In the example, this would create a user with the Matrix Identifier
+`@your-username:example.org`.
+
+::: {.warning}
+When using [](#opt-services.matrix-synapse.settings.registration_shared_secret), the secret
+will end up in the world-readable store. Instead it's recommended to deploy the secret
+in an additional file like this:
+
+  - Create a file with the following contents:
+
+    ```
+    registration_shared_secret: your-very-secret-secret
+    ```
+  - Deploy the file with a secret-manager such as
+    [{option}`deployment.keys`](https://nixops.readthedocs.io/en/latest/overview.html#managing-keys)
+    from {manpage}`nixops(1)` or [sops-nix](https://github.com/Mic92/sops-nix/) to
+    e.g. {file}`/run/secrets/matrix-shared-secret` and ensure that it's readable
+    by `matrix-synapse`.
+  - Include the file like this in your configuration:
+
+    ```
+    {
+      services.matrix-synapse.extraConfigFiles = [
+        "/run/secrets/matrix-shared-secret"
+      ];
+    }
+    ```
+:::
+
+::: {.note}
+It's also possible to user alternative authentication mechanism such as
+[LDAP (via `matrix-synapse-ldap3`)](https://github.com/matrix-org/matrix-synapse-ldap3)
+or [OpenID](https://matrix-org.github.io/synapse/latest/openid.html).
+:::
+
+## Element (formerly known as Riot) Web Client {#module-services-matrix-element-web}
+
+[Element Web](https://github.com/vector-im/riot-web/) is
+the reference web client for Matrix and developed by the core team at
+matrix.org. Element was formerly known as Riot.im, see the
+[Element introductory blog post](https://element.io/blog/welcome-to-element/)
+for more information. The following snippet can be optionally added to the code before
+to complete the synapse installation with a web client served at
+`https://element.myhostname.example.org` and
+`https://element.example.org`. Alternatively, you can use the hosted
+copy at <https://app.element.io/>,
+or use other web clients or native client applications. Due to the
+`/.well-known` urls set up done above, many clients should
+fill in the required connection details automatically when you enter your
+Matrix Identifier. See
+[Try Matrix Now!](https://matrix.org/docs/projects/try-matrix-now.html)
+for a list of existing clients and their supported featureset.
+```
+{
+  services.nginx.virtualHosts."element.${fqdn}" = {
+    enableACME = true;
+    forceSSL = true;
+    serverAliases = [
+      "element.${config.networking.domain}"
+    ];
+
+    root = pkgs.element-web.override {
+      conf = {
+        default_server_config = clientConfig; # see `clientConfig` from the snippet above.
+      };
+    };
+  };
+}
+```
+
+::: {.note}
+The Element developers do not recommend running Element and your Matrix
+homeserver on the same fully-qualified domain name for security reasons. In
+the example, this means that you should not reuse the
+`myhostname.example.org` virtualHost to also serve Element,
+but instead serve it on a different subdomain, like
+`element.example.org` in the example. See the
+[Element Important Security Notes](https://github.com/vector-im/element-web/tree/v1.10.0#important-security-notes)
+for more information on this subject.
+:::
diff --git a/nixos/modules/services/matrix/synapse.nix b/nixos/modules/services/matrix/synapse.nix
index 3087d879b9d..3dca3ff94f2 100644
--- a/nixos/modules/services/matrix/synapse.nix
+++ b/nixos/modules/services/matrix/synapse.nix
@@ -60,7 +60,7 @@ in {
     '')
     (mkRemovedOptionModule [ "services" "matrix-synapse" "create_local_database" ] ''
       Database configuration must be done manually. An exemplary setup is demonstrated in
-      <nixpkgs/nixos/tests/matrix-synapse.nix>
+      <nixpkgs/nixos/tests/matrix/synapse.nix>
     '')
     (mkRemovedOptionModule [ "services" "matrix-synapse" "web_client" ] "")
     (mkRemovedOptionModule [ "services" "matrix-synapse" "room_invite_state_types" ] ''
@@ -636,6 +636,7 @@ in {
 
             trusted_key_servers = mkOption {
               type = types.listOf (types.submodule {
+                freeformType = format.type;
                 options = {
                   server_name = mkOption {
                     type = types.str;
@@ -644,22 +645,6 @@ in {
                       Hostname of the trusted server.
                     '';
                   };
-
-                  verify_keys = mkOption {
-                    type = types.nullOr (types.attrsOf types.str);
-                    default = null;
-                    example = literalExpression ''
-                      {
-                        "ed25519:auto" = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw";
-                      }
-                    '';
-                    description = lib.mdDoc ''
-                      Attribute set from key id to base64 encoded public key.
-
-                      If specified synapse will check that the response is signed
-                      by at least one of the given keys.
-                    '';
-                  };
                 };
               });
               default = [ {
@@ -711,7 +696,7 @@ in {
 
             If you
             - try to deploy a fresh synapse, you need to configure the database yourself. An example
-              for this can be found in <nixpkgs/nixos/tests/matrix-synapse.nix>
+              for this can be found in <nixpkgs/nixos/tests/matrix/synapse.nix>
             - update your existing matrix-synapse instance, you simply need to add `services.postgresql.enable = true`
               to your configuration.
 
@@ -755,8 +740,8 @@ in {
         Group = "matrix-synapse";
         WorkingDirectory = cfg.dataDir;
         ExecStartPre = [ ("+" + (pkgs.writeShellScript "matrix-synapse-fix-permissions" ''
-          chown matrix-synapse:matrix-synapse ${cfg.dataDir}/homeserver.signing.key
-          chmod 0600 ${cfg.dataDir}/homeserver.signing.key
+          chown matrix-synapse:matrix-synapse ${cfg.settings.signing_key_path}
+          chmod 0600 ${cfg.settings.signing_key_path}
         '')) ];
         ExecStart = ''
           ${cfg.package}/bin/synapse_homeserver \
@@ -801,7 +786,7 @@ in {
 
   meta = {
     buildDocsInSandbox = false;
-    doc = ./synapse.xml;
+    doc = ./synapse.md;
     maintainers = teams.matrix.members;
   };
 
diff --git a/nixos/modules/services/matrix/synapse.xml b/nixos/modules/services/matrix/synapse.xml
deleted file mode 100644
index 40ad72173a5..00000000000
--- a/nixos/modules/services/matrix/synapse.xml
+++ /dev/null
@@ -1,276 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-matrix">
- <title>Matrix</title>
- <para>
-  <link xlink:href="https://matrix.org/">Matrix</link> is an open standard for
-  interoperable, decentralised, real-time communication over IP. It can be used
-  to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things
-  communication - or anywhere you need a standard HTTP API for publishing and
-  subscribing to data whilst tracking the conversation history.
- </para>
- <para>
-  This chapter will show you how to set up your own, self-hosted Matrix
-  homeserver using the Synapse reference homeserver, and how to serve your own
-  copy of the Element web client. See the
-  <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try
-  Matrix Now!</link> overview page for links to Element Apps for Android and iOS,
-  desktop clients, as well as bridges to other networks and other projects
-  around Matrix.
- </para>
- <section xml:id="module-services-matrix-synapse">
-  <title>Synapse Homeserver</title>
-
-  <para>
-   <link xlink:href="https://github.com/matrix-org/synapse">Synapse</link> is
-   the reference homeserver implementation of Matrix from the core development
-   team at matrix.org. The following configuration example will set up a
-   synapse server for the <literal>example.org</literal> domain, served from
-   the host <literal>myhostname.example.org</literal>. For more information,
-   please refer to the
-   <link xlink:href="https://matrix-org.github.io/synapse/latest/setup/installation.html">
-   installation instructions of Synapse </link>.
-<programlisting>
-{ pkgs, lib, config, ... }:
-let
-  fqdn = "${config.networking.hostName}.${config.networking.domain}";
-  clientConfig = {
-    "m.homeserver".base_url = "https://${fqdn}";
-    "m.identity_server" = {};
-  };
-  serverConfig."m.server" = "${config.services.matrix-synapse.settings.server_name}:443";
-  mkWellKnown = data: ''
-    add_header Content-Type application/json;
-    add_header Access-Control-Allow-Origin *;
-    return 200 '${builtins.toJSON data}';
-  '';
-in {
-  <xref linkend="opt-networking.hostName" /> = "myhostname";
-  <xref linkend="opt-networking.domain" /> = "example.org";
-  <xref linkend="opt-networking.firewall.allowedTCPPorts" /> = [ 80 443 ];
-
-  <xref linkend="opt-services.postgresql.enable" /> = true;
-  <xref linkend="opt-services.postgresql.initialScript" /> = pkgs.writeText "synapse-init.sql" ''
-    CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
-    CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
-      TEMPLATE template0
-      LC_COLLATE = "C"
-      LC_CTYPE = "C";
-  '';
-
-  services.nginx = {
-    <link linkend="opt-services.nginx.enable">enable</link> = true;
-    <link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true;
-    <link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true;
-    <link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true;
-    <link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true;
-    <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
-      "${config.networking.domain}" = { <co xml:id='ex-matrix-synapse-dns' />
-        <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
-        <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."= /.well-known/matrix/server".extraConfig</link> = mkWellKnown serverConfig; <co xml:id='ex-matrix-synapse-well-known-server' />
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."= /.well-known/matrix/client".extraConfig</link> = mkWellKnown clientConfig; <co xml:id='ex-matrix-synapse-well-known-client' />
-      };
-      "${fqdn}" = {
-        <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
-        <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."/".extraConfig</link> = '' <co xml:id='ex-matrix-synapse-rev-default' />
-          return 404;
-        '';
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.proxyPass">locations."/_matrix".proxyPass</link> = "http://[::1]:8008"; <co xml:id='ex-matrix-synapse-rev-proxy-pass' />
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.proxyPass">locations."/_synapse/client".proxyPass</link> = "http://[::1]:8008"; <co xml:id='ex-matrix-synapse-rev-client' />
-      };
-    };
-  };
-
-  services.matrix-synapse = {
-    <link linkend="opt-services.matrix-synapse.enable">enable</link> = true;
-    <link linkend="opt-services.matrix-synapse.settings.server_name">settings.server_name</link> = config.networking.domain;
-    <link linkend="opt-services.matrix-synapse.settings.listeners">settings.listeners</link> = [
-      { <link linkend="opt-services.matrix-synapse.settings.listeners._.port">port</link> = 8008;
-        <link linkend="opt-services.matrix-synapse.settings.listeners._.bind_addresses">bind_addresses</link> = [ "::1" ];
-        <link linkend="opt-services.matrix-synapse.settings.listeners._.type">type</link> = "http";
-        <link linkend="opt-services.matrix-synapse.settings.listeners._.tls">tls</link> = false;
-        <link linkend="opt-services.matrix-synapse.settings.listeners._.x_forwarded">x_forwarded</link> = true;
-        <link linkend="opt-services.matrix-synapse.settings.listeners._.resources">resources</link> = [ {
-          <link linkend="opt-services.matrix-synapse.settings.listeners._.resources._.names">names</link> = [ "client" "federation" ];
-          <link linkend="opt-services.matrix-synapse.settings.listeners._.resources._.compress">compress</link> = true;
-        } ];
-      }
-    ];
-  };
-}
-</programlisting>
-  </para>
-  <calloutlist>
-   <callout arearefs='ex-matrix-synapse-dns'>
-    <para>
-     If the <code>A</code> and <code>AAAA</code> DNS records on
-     <literal>example.org</literal> do not point on the same host as the records
-     for <code>myhostname.example.org</code>, you can easily move the
-     <code>/.well-known</code> virtualHost section of the code to the host that
-     is serving <literal>example.org</literal>, while the rest stays on
-     <literal>myhostname.example.org</literal> with no other changes required.
-     This pattern also allows to seamlessly move the homeserver from
-     <literal>myhostname.example.org</literal> to
-     <literal>myotherhost.example.org</literal> by only changing the
-     <code>/.well-known</code> redirection target.
-    </para>
-   </callout>
-   <callout arearefs='ex-matrix-synapse-well-known-server'>
-    <para>
-     This section is not needed if the <link linkend="opt-services.matrix-synapse.settings.server_name">server_name</link>
-     of <package>matrix-synapse</package> is equal to the domain (i.e.
-     <literal>example.org</literal> from <literal>@foo:example.org</literal>)
-     and the federation port is 8448.
-     Further reference can be found in the <link xlink:href="https://matrix-org.github.io/synapse/latest/delegate.html">docs
-     about delegation</link>.
-    </para>
-   </callout>
-   <callout arearefs='ex-matrix-synapse-well-known-client'>
-    <para>
-     This is usually needed for homeserver discovery (from e.g. other Matrix clients).
-     Further reference can be found in the <link xlink:href="https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient">upstream docs</link>
-    </para>
-   </callout>
-   <callout arearefs='ex-matrix-synapse-rev-default'>
-    <para>
-     It's also possible to do a redirect here or something else, this vhost is not
-     needed for Matrix. It's recommended though to <emphasis>not put</emphasis> element
-     here, see also the <link linkend='ex-matrix-synapse-rev-default'>section about Element</link>.
-    </para>
-   </callout>
-   <callout arearefs='ex-matrix-synapse-rev-proxy-pass'>
-    <para>
-     Forward all Matrix API calls to the synapse Matrix homeserver. A trailing slash
-     <emphasis>must not</emphasis> be used here.
-    </para>
-   </callout>
-   <callout arearefs='ex-matrix-synapse-rev-client'>
-    <para>
-     Forward requests for e.g. SSO and password-resets.
-    </para>
-   </callout>
-  </calloutlist>
- </section>
- <section xml:id="module-services-matrix-register-users">
-  <title>Registering Matrix users</title>
-  <para>
-   If you want to run a server with public registration by anybody, you can
-   then enable <literal><link linkend="opt-services.matrix-synapse.settings.enable_registration">services.matrix-synapse.settings.enable_registration</link> =
-   true;</literal>. Otherwise, or you can generate a registration secret with
-   <command>pwgen -s 64 1</command> and set it with
-   <option><link linkend="opt-services.matrix-synapse.settings.registration_shared_secret">services.matrix-synapse.settings.registration_shared_secret</link></option>.
-   To create a new user or admin, run the following after you have set the secret
-   and have rebuilt NixOS:
-<screen>
-<prompt>$ </prompt>nix-shell -p matrix-synapse
-<prompt>$ </prompt>register_new_matrix_user -k <replaceable>your-registration-shared-secret</replaceable> http://localhost:8008
-<prompt>New user localpart: </prompt><replaceable>your-username</replaceable>
-<prompt>Password:</prompt>
-<prompt>Confirm password:</prompt>
-<prompt>Make admin [no]:</prompt>
-Success!
-</screen>
-   In the example, this would create a user with the Matrix Identifier
-   <literal>@your-username:example.org</literal>.
-   <warning>
-    <para>
-     When using <xref linkend="opt-services.matrix-synapse.settings.registration_shared_secret" />, the secret
-     will end up in the world-readable store. Instead it's recommended to deploy the secret
-     in an additional file like this:
-     <itemizedlist>
-      <listitem>
-       <para>
-        Create a file with the following contents:
-<programlisting>registration_shared_secret: your-very-secret-secret</programlisting>
-       </para>
-      </listitem>
-      <listitem>
-       <para>
-        Deploy the file with a secret-manager such as <link xlink:href="https://nixops.readthedocs.io/en/latest/overview.html#managing-keys"><option>deployment.keys</option></link>
-        from <citerefentry><refentrytitle>nixops</refentrytitle><manvolnum>1</manvolnum></citerefentry>
-        or <link xlink:href="https://github.com/Mic92/sops-nix/">sops-nix</link> to
-        e.g. <filename>/run/secrets/matrix-shared-secret</filename> and ensure that it's readable
-        by <package>matrix-synapse</package>.
-       </para>
-      </listitem>
-      <listitem>
-       <para>
-        Include the file like this in your configuration:
-<programlisting>
-{
-  <xref linkend="opt-services.matrix-synapse.extraConfigFiles" /> = [
-    "/run/secrets/matrix-shared-secret"
-  ];
-}
-</programlisting>
-       </para>
-      </listitem>
-     </itemizedlist>
-    </para>
-   </warning>
-  </para>
-  <note>
-   <para>
-    It's also possible to user alternative authentication mechanism such as
-    <link xlink:href="https://github.com/matrix-org/matrix-synapse-ldap3">LDAP (via <literal>matrix-synapse-ldap3</literal>)</link>
-    or <link xlink:href="https://matrix-org.github.io/synapse/latest/openid.html">OpenID</link>.
-   </para>
-  </note>
- </section>
- <section xml:id="module-services-matrix-element-web">
-  <title>Element (formerly known as Riot) Web Client</title>
-
-  <para>
-   <link xlink:href="https://github.com/vector-im/riot-web/">Element Web</link> is
-   the reference web client for Matrix and developed by the core team at
-   matrix.org. Element was formerly known as Riot.im, see the
-   <link xlink:href="https://element.io/blog/welcome-to-element/">Element introductory blog post</link>
-   for more information. The following snippet can be optionally added to the code before
-   to complete the synapse installation with a web client served at
-   <code>https://element.myhostname.example.org</code> and
-   <code>https://element.example.org</code>. Alternatively, you can use the hosted
-   copy at <link xlink:href="https://app.element.io/">https://app.element.io/</link>,
-   or use other web clients or native client applications. Due to the
-   <literal>/.well-known</literal> urls set up done above, many clients should
-   fill in the required connection details automatically when you enter your
-   Matrix Identifier. See
-   <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try
-   Matrix Now!</link> for a list of existing clients and their supported
-   featureset.
-<programlisting>
-{
-  services.nginx.virtualHosts."element.${fqdn}" = {
-    <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
-    <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
-    <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [
-      "element.${config.networking.domain}"
-    ];
-
-    <link linkend="opt-services.nginx.virtualHosts._name_.root">root</link> = pkgs.element-web.override {
-      conf = {
-        default_server_config = clientConfig; # see `clientConfig` from the snippet above.
-      };
-    };
-  };
-}
-</programlisting>
-  </para>
-
-  <note>
-   <para>
-    The Element developers do not recommend running Element and your Matrix
-    homeserver on the same fully-qualified domain name for security reasons. In
-    the example, this means that you should not reuse the
-    <literal>myhostname.example.org</literal> virtualHost to also serve Element,
-    but instead serve it on a different subdomain, like
-    <literal>element.example.org</literal> in the example. See the
-    <link xlink:href="https://github.com/vector-im/element-web/tree/v1.10.0#important-security-notes">Element
-    Important Security Notes</link> for more information on this subject.
-   </para>
-  </note>
- </section>
-</chapter>
diff --git a/nixos/modules/services/misc/ankisyncd.nix b/nixos/modules/services/misc/ankisyncd.nix
index 5198b824202..7be8dc7dab8 100644
--- a/nixos/modules/services/misc/ankisyncd.nix
+++ b/nixos/modules/services/misc/ankisyncd.nix
@@ -9,22 +9,16 @@ let
 
   stateDir = "/var/lib/${name}";
 
-  authDbPath = "${stateDir}/auth.db";
+  toml = pkgs.formats.toml {};
 
-  sessionDbPath = "${stateDir}/session.db";
-
-  configFile = pkgs.writeText "ankisyncd.conf" (lib.generators.toINI {} {
-    sync_app = {
+  configFile = toml.generate "ankisyncd.conf" {
+    listen = {
       host = cfg.host;
       port = cfg.port;
-      data_root = stateDir;
-      auth_db_path = authDbPath;
-      session_db_path = sessionDbPath;
-
-      base_url = "/sync/";
-      base_media_url = "/msync/";
     };
-  });
+    paths.root_dir = stateDir;
+    # encryption.ssl_enable / cert_file / key_file
+  };
 in
   {
     options.services.ankisyncd = {
@@ -59,8 +53,6 @@ in
     config = mkIf cfg.enable {
       networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
 
-      environment.etc."ankisyncd/ankisyncd.conf".source = configFile;
-
       systemd.services.ankisyncd = {
         description = "ankisyncd - Anki sync server";
         after = [ "network.target" ];
@@ -71,7 +63,7 @@ in
           Type = "simple";
           DynamicUser = true;
           StateDirectory = name;
-          ExecStart = "${cfg.package}/bin/ankisyncd";
+          ExecStart = "${cfg.package}/bin/ankisyncd --config ${configFile}";
           Restart = "always";
         };
       };
diff --git a/nixos/modules/services/misc/atuin.nix b/nixos/modules/services/misc/atuin.nix
index c94852e3aad..202bd4dfca1 100644
--- a/nixos/modules/services/misc/atuin.nix
+++ b/nixos/modules/services/misc/atuin.nix
@@ -8,7 +8,7 @@ in
 {
   options = {
     services.atuin = {
-      enable = mkEnableOption (mdDoc "Enable server for shell history sync with atuin.");
+      enable = mkEnableOption (mdDoc "Enable server for shell history sync with atuin");
 
       openRegistration = mkOption {
         type = types.bool;
@@ -28,6 +28,12 @@ in
         description = mdDoc "The host address the atuin server should listen on.";
       };
 
+      maxHistoryLength = mkOption {
+        type = types.int;
+        default = 8192;
+        description = mdDoc "The max length of each history item the atuin server should store.";
+      };
+
       port = mkOption {
         type = types.port;
         default = 8888;
@@ -40,6 +46,13 @@ in
         description = mdDoc "Open ports in the firewall for the atuin server.";
       };
 
+      database = {
+        createLocally = mkOption {
+          type = types.bool;
+          default = true;
+          description = lib.mdDoc "Create the database and database user locally.";
+        };
+      };
     };
   };
 
@@ -59,7 +72,8 @@ in
 
     systemd.services.atuin = {
       description = "atuin server";
-      after = [ "network.target" "postgresql.service" ];
+      requires = lib.optionals cfg.database.createLocally [ "postgresql.service" ];
+      after = [ "network.target" ] ++ lib.optionals cfg.database.createLocally [ "postgresql.service" ] ;
       wantedBy = [ "multi-user.target" ];
 
       serviceConfig = {
@@ -72,8 +86,9 @@ in
       environment = {
         ATUIN_HOST = cfg.host;
         ATUIN_PORT = toString cfg.port;
+        ATUIN_MAX_HISTORY_LENGTH = toString cfg.maxHistoryLength;
         ATUIN_OPEN_REGISTRATION = boolToString cfg.openRegistration;
-        ATUIN_DB_URI = "postgresql:///atuin";
+        ATUIN_DB_URI =  mkIf cfg.database.createLocally "postgresql:///atuin";
         ATUIN_PATH = cfg.path;
         ATUIN_CONFIG_DIR = "/run/atuin"; # required to start, but not used as configuration is via environment variables
       };
diff --git a/nixos/modules/services/misc/autorandr.nix b/nixos/modules/services/misc/autorandr.nix
index 072064143db..aa96acb6130 100644
--- a/nixos/modules/services/misc/autorandr.nix
+++ b/nixos/modules/services/misc/autorandr.nix
@@ -254,6 +254,12 @@ in {
         '';
       };
 
+      ignoreLid = mkOption {
+        default = false;
+        type = types.bool;
+        description = lib.mdDoc "Treat outputs as connected even if their lids are closed";
+      };
+
       hooks = mkOption {
         type = hooksModule;
         description = lib.mdDoc "Global hook scripts";
@@ -340,7 +346,13 @@ in {
       startLimitIntervalSec = 5;
       startLimitBurst = 1;
       serviceConfig = {
-        ExecStart = "${pkgs.autorandr}/bin/autorandr --batch --change --default ${cfg.defaultTarget}";
+        ExecStart = ''
+          ${pkgs.autorandr}/bin/autorandr \
+            --batch \
+            --change \
+            --default ${cfg.defaultTarget} \
+            ${optionalString cfg.ignoreLid "--ignore-lid"}
+        '';
         Type = "oneshot";
         RemainAfterExit = false;
         KillMode = "process";
diff --git a/nixos/modules/services/misc/autosuspend.nix b/nixos/modules/services/misc/autosuspend.nix
new file mode 100644
index 00000000000..b3e362533a0
--- /dev/null
+++ b/nixos/modules/services/misc/autosuspend.nix
@@ -0,0 +1,230 @@
+{ config, pkgs, lib, ... }:
+let
+  inherit (lib) mapAttrs' nameValuePair filterAttrs types mkEnableOption
+    mdDoc mkPackageOptionMD mkOption literalExpression mkIf flatten
+    maintainers attrValues;
+
+  cfg = config.services.autosuspend;
+
+  settingsFormat = pkgs.formats.ini { };
+
+  checks =
+    mapAttrs'
+      (n: v: nameValuePair "check.${n}" (filterAttrs (_: v: v != null) v))
+      cfg.checks;
+  wakeups =
+    mapAttrs'
+      (n: v: nameValuePair "wakeup.${n}" (filterAttrs (_: v: v != null) v))
+      cfg.wakeups;
+
+  # Whether the given check is enabled
+  hasCheck = class:
+    (filterAttrs
+      (n: v: v.enabled && (if v.class == null then n else v.class) == class)
+      cfg.checks)
+    != { };
+
+  # Dependencies needed by specific checks
+  dependenciesForChecks = {
+    "Smb" = pkgs.samba;
+    "XIdleTime" = [ pkgs.xprintidle pkgs.sudo ];
+  };
+
+  autosuspend-conf =
+    settingsFormat.generate "autosuspend.conf" ({ general = cfg.settings; } // checks // wakeups);
+
+  autosuspend = cfg.package;
+
+  checkType = types.submodule {
+    freeformType = settingsFormat.type.nestedTypes.elemType;
+
+    options.enabled = mkEnableOption (mdDoc "this activity check") // { default = true; };
+
+    options.class = mkOption {
+      default = null;
+      type = with types; nullOr (enum [
+        "ActiveCalendarEvent"
+        "ActiveConnection"
+        "ExternalCommand"
+        "JsonPath"
+        "Kodi"
+        "KodiIdleTime"
+        "LastLogActivity"
+        "Load"
+        "LogindSessionsIdle"
+        "Mpd"
+        "NetworkBandwidth"
+        "Ping"
+        "Processes"
+        "Smb"
+        "Users"
+        "XIdleTime"
+        "XPath"
+      ]);
+      description = mdDoc ''
+        Name of the class implementing the check.  If this option is not specified, the check's
+        name must represent a valid internal check class.
+      '';
+    };
+  };
+
+  wakeupType = types.submodule {
+    freeformType = settingsFormat.type.nestedTypes.elemType;
+
+    options.enabled = mkEnableOption (mdDoc "this wake-up check") // { default = true; };
+
+    options.class = mkOption {
+      default = null;
+      type = with types; nullOr (enum [
+        "Calendar"
+        "Command"
+        "File"
+        "Periodic"
+        "SystemdTimer"
+        "XPath"
+        "XPathDelta"
+      ]);
+      description = mdDoc ''
+        Name of the class implementing the check.  If this option is not specified, the check's
+        name must represent a valid internal check class.
+      '';
+    };
+  };
+in
+{
+  options = {
+    services.autosuspend = {
+      enable = mkEnableOption (mdDoc "the autosuspend daemon");
+
+      package = mkPackageOptionMD pkgs "autosuspend" { };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = settingsFormat.type.nestedTypes.elemType;
+
+          options = {
+            # Provide reasonable defaults for these two (required) options
+            suspend_cmd = mkOption {
+              default = "systemctl suspend";
+              type = with types; str;
+              description = mdDoc ''
+                The command to execute in case the host shall be suspended. This line can contain
+                additional command line arguments to the command to execute.
+              '';
+            };
+            wakeup_cmd = mkOption {
+              default = ''sh -c 'echo 0 > /sys/class/rtc/rtc0/wakealarm && echo {timestamp:.0f} > /sys/class/rtc/rtc0/wakealarm' '';
+              type = with types; str;
+              description = mdDoc ''
+                The command to execute for scheduling a wake up of the system. The given string is
+                processed using Python’s `str.format()` and a format argument called `timestamp`
+                encodes the UTC timestamp of the planned wake up time (float). Additionally `iso`
+                can be used to acquire the timestamp in ISO 8601 format.
+              '';
+            };
+          };
+        };
+        default = { };
+        example = literalExpression ''
+          {
+            enable = true;
+            interval = 30;
+            idle_time = 120;
+          }
+        '';
+        description = mdDoc ''
+          Configuration for autosuspend, see
+          <https://autosuspend.readthedocs.io/en/latest/configuration_file.html#general-configuration>
+          for supported values.
+        '';
+      };
+
+      checks = mkOption {
+        default = { };
+        type = with types; attrsOf checkType;
+        description = mdDoc ''
+          Checks for activity.  For more information, see:
+           - <https://autosuspend.readthedocs.io/en/latest/configuration_file.html#activity-check-configuration>
+           - <https://autosuspend.readthedocs.io/en/latest/available_checks.html>
+        '';
+        example = literalExpression ''
+          {
+            # Basic activity check configuration.
+            # The check class name is derived from the section header (Ping in this case).
+            # Remember to enable desired checks. They are disabled by default.
+            Ping = {
+              hosts = "192.168.0.7";
+            };
+
+            # This check is disabled.
+            Smb.enabled = false;
+
+            # Example for a custom check name.
+            # This will use the Users check with the custom name RemoteUsers.
+            # Custom names are necessary in case a check class is used multiple times.
+            # Custom names can also be used for clarification.
+            RemoteUsers = {
+              class = "Users";
+              name = ".*";
+              terminal = ".*";
+              host = "[0-9].*";
+            };
+
+            # Here the Users activity check is used again with different settings and a different name
+            LocalUsers = {
+              class = "Users";
+              name = ".*";
+              terminal = ".*";
+              host = "localhost";
+            };
+          }
+        '';
+      };
+
+      wakeups = mkOption {
+        default = { };
+        type = with types; attrsOf wakeupType;
+        description = mdDoc ''
+          Checks for wake up.  For more information, see:
+           - <https://autosuspend.readthedocs.io/en/latest/configuration_file.html#wake-up-check-configuration>
+           - <https://autosuspend.readthedocs.io/en/latest/available_wakeups.html>
+        '';
+        example = literalExpression ''
+          {
+            # Wake up checks reuse the same configuration mechanism as activity checks.
+            Calendar = {
+              url = "http://example.org/test.ics";
+            };
+          }
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.autosuspend = {
+      description = "A daemon to suspend your server in case of inactivity";
+      documentation = [ "https://autosuspend.readthedocs.io/en/latest/systemd_integration.html" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = flatten (attrValues (filterAttrs (n: _: hasCheck n) dependenciesForChecks));
+      serviceConfig = {
+        ExecStart = ''${autosuspend}/bin/autosuspend -l ${autosuspend}/etc/autosuspend-logging.conf -c ${autosuspend-conf} daemon'';
+      };
+    };
+
+    systemd.services.autosuspend-detect-suspend = {
+      description = "Notifies autosuspend about suspension";
+      documentation = [ "https://autosuspend.readthedocs.io/en/latest/systemd_integration.html" ];
+      wantedBy = [ "sleep.target" ];
+      after = [ "sleep.target" ];
+      serviceConfig = {
+        ExecStart = ''${autosuspend}/bin/autosuspend -l ${autosuspend}/etc/autosuspend-logging.conf -c ${autosuspend-conf} presuspend'';
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with maintainers; [ xlambein ];
+  };
+}
diff --git a/nixos/modules/services/misc/calibre-server.nix b/nixos/modules/services/misc/calibre-server.nix
index 77c60381a31..e1ddae1de1f 100644
--- a/nixos/modules/services/misc/calibre-server.nix
+++ b/nixos/modules/services/misc/calibre-server.nix
@@ -6,6 +6,17 @@ let
 
   cfg = config.services.calibre-server;
 
+  documentationLink = "https://manual.calibre-ebook.com";
+  generatedDocumentationLink = documentationLink + "/generated/en/calibre-server.html";
+
+  execFlags = (concatStringsSep " "
+    (mapAttrsToList (k: v: "${k} ${toString v}") (filterAttrs (name: value: value != null) {
+      "--listen-on" = cfg.host;
+      "--port" = cfg.port;
+      "--auth-mode" = cfg.auth.mode;
+      "--userdb" = cfg.auth.userDb;
+    }) ++ [(optionalString (cfg.auth.enable == true) "--enable-auth")])
+  );
 in
 
 {
@@ -18,52 +29,100 @@ in
     )
   ];
 
-  ###### interface
-
   options = {
     services.calibre-server = {
 
       enable = mkEnableOption (lib.mdDoc "calibre-server");
+      package = lib.mkPackageOptionMD pkgs "calibre" { };
 
       libraries = mkOption {
+        type = types.listOf types.path;
+        default = [ "/var/lib/calibre-server" ];
         description = lib.mdDoc ''
+          Make sure each library path is initialized before service startup.
           The directories of the libraries to serve. They must be readable for the user under which the server runs.
+          See the [calibredb documentation](${documentationLink}/generated/en/calibredb.html#add) for details.
         '';
-        type = types.listOf types.path;
       };
 
       user = mkOption {
-        description = lib.mdDoc "The user under which calibre-server runs.";
         type = types.str;
         default = "calibre-server";
+        description = lib.mdDoc "The user under which calibre-server runs.";
       };
 
       group = mkOption {
-        description = lib.mdDoc "The group under which calibre-server runs.";
         type = types.str;
         default = "calibre-server";
+        description = lib.mdDoc "The group under which calibre-server runs.";
       };
 
-    };
-  };
+      host = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        example = "::1";
+        description = lib.mdDoc ''
+          The interface on which to listen for connections.
+          See the [calibre-server documentation](${generatedDocumentationLink}#cmdoption-calibre-server-listen-on) for details.
+        '';
+      };
+
+      port = mkOption {
+        default = 8080;
+        type = types.port;
+        description = lib.mdDoc ''
+          The port on which to listen for connections.
+          See the [calibre-server documentation](${generatedDocumentationLink}#cmdoption-calibre-server-port) for details.
+        '';
+      };
 
+      auth = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = lib.mdDoc ''
+            Password based authentication to access the server.
+            See the [calibre-server documentation](${generatedDocumentationLink}#cmdoption-calibre-server-enable-auth) for details.
+          '';
+        };
 
-  ###### implementation
+        mode = mkOption {
+          type = types.enum [ "auto" "basic" "digest" ];
+          default = "auto";
+          description = lib.mdDoc ''
+            Choose the type of authentication used.
+            Set the HTTP authentication mode used by the server.
+            See the [calibre-server documentation](${generatedDocumentationLink}#cmdoption-calibre-server-auth-mode) for details.
+          '';
+        };
+
+        userDb = mkOption {
+          default = null;
+          type = types.nullOr types.path;
+          description = lib.mdDoc ''
+            Choose users database file to use for authentication.
+            Make sure users database file is initialized before service startup.
+            See the [calibre-server documentation](${documentationLink}/server.html#managing-user-accounts-from-the-command-line-only) for details.
+          '';
+        };
+      };
+    };
+  };
 
   config = mkIf cfg.enable {
 
     systemd.services.calibre-server = {
-        description = "Calibre Server";
-        after = [ "network.target" ];
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          User = cfg.user;
-          Restart = "always";
-          ExecStart = "${pkgs.calibre}/bin/calibre-server ${lib.concatStringsSep " " cfg.libraries}";
-        };
-
+      description = "Calibre Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = cfg.user;
+        Restart = "always";
+        ExecStart = "${cfg.package}/bin/calibre-server ${lib.concatStringsSep " " cfg.libraries} ${execFlags}";
       };
 
+    };
+
     environment.systemPackages = [ pkgs.calibre ];
 
     users.users = optionalAttrs (cfg.user == "calibre-server") {
@@ -83,4 +142,5 @@ in
 
   };
 
+  meta.maintainers = with lib.maintainers; [ gaelreyrol ];
 }
diff --git a/nixos/modules/services/misc/disnix.nix b/nixos/modules/services/misc/disnix.nix
index 1cdfeef57ce..13c57ce6b85 100644
--- a/nixos/modules/services/misc/disnix.nix
+++ b/nixos/modules/services/misc/disnix.nix
@@ -87,8 +87,8 @@ in
         environment = {
           HOME = "/root";
         }
-        // (if config.environment.variables ? DYSNOMIA_CONTAINERS_PATH then { inherit (config.environment.variables) DYSNOMIA_CONTAINERS_PATH; } else {})
-        // (if config.environment.variables ? DYSNOMIA_MODULES_PATH then { inherit (config.environment.variables) DYSNOMIA_MODULES_PATH; } else {});
+        // (optionalAttrs (config.environment.variables ? DYSNOMIA_CONTAINERS_PATH) { inherit (config.environment.variables) DYSNOMIA_CONTAINERS_PATH; })
+        // (optionalAttrs (config.environment.variables ? DYSNOMIA_MODULES_PATH) { inherit (config.environment.variables) DYSNOMIA_MODULES_PATH; });
 
         serviceConfig.ExecStart = "${cfg.package}/bin/disnix-service";
       };
diff --git a/nixos/modules/services/misc/docker-registry.nix b/nixos/modules/services/misc/docker-registry.nix
index 98edb413f3c..b0e91063463 100644
--- a/nixos/modules/services/misc/docker-registry.nix
+++ b/nixos/modules/services/misc/docker-registry.nix
@@ -15,9 +15,7 @@ let
     storage = {
       cache.blobdescriptor = blobCache;
       delete.enabled = cfg.enableDelete;
-    } // (if cfg.storagePath != null
-          then { filesystem.rootdirectory = cfg.storagePath; }
-          else {});
+    } // (optionalAttrs (cfg.storagePath != null) { filesystem.rootdirectory = cfg.storagePath; });
     http = {
       addr = "${cfg.listenAddress}:${builtins.toString cfg.port}";
       headers.X-Content-Type-Options = ["nosniff"];
@@ -49,6 +47,14 @@ in {
   options.services.dockerRegistry = {
     enable = mkEnableOption (lib.mdDoc "Docker Registry");
 
+    package = mkOption {
+      type = types.package;
+      description = mdDoc "Which Docker registry package to use.";
+      default = pkgs.docker-distribution;
+      defaultText = literalExpression "pkgs.docker-distribution";
+      example = literalExpression "pkgs.gitlab-container-registry";
+    };
+
     listenAddress = mkOption {
       description = lib.mdDoc "Docker registry host or ip to bind to.";
       default = "127.0.0.1";
@@ -117,7 +123,7 @@ in {
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       script = ''
-        ${pkgs.docker-distribution}/bin/registry serve ${configFile}
+        ${cfg.package}/bin/registry serve ${configFile}
       '';
 
       serviceConfig = {
@@ -136,7 +142,7 @@ in {
       serviceConfig.Type = "oneshot";
 
       script = ''
-        ${pkgs.docker-distribution}/bin/registry garbage-collect ${configFile}
+        ${cfg.package}/bin/registry garbage-collect ${configFile}
         /run/current-system/systemd/bin/systemctl restart docker-registry.service
       '';
 
@@ -144,12 +150,10 @@ in {
     };
 
     users.users.docker-registry =
-      (if cfg.storagePath != null
-      then {
+      (optionalAttrs (cfg.storagePath != null) {
         createHome = true;
         home = cfg.storagePath;
-      }
-      else {}) // {
+      }) // {
         group = "docker-registry";
         isSystemUser = true;
       };
diff --git a/nixos/modules/services/misc/etcd.nix b/nixos/modules/services/misc/etcd.nix
index 3343e94778a..7bc7a949911 100644
--- a/nixos/modules/services/misc/etcd.nix
+++ b/nixos/modules/services/misc/etcd.nix
@@ -15,6 +15,8 @@ in {
       type = types.bool;
     };
 
+    package = mkPackageOptionMD pkgs "etcd" { };
+
     name = mkOption {
       description = lib.mdDoc "Etcd unique node name.";
       default = config.networking.hostName;
@@ -167,10 +169,11 @@ in {
         ETCD_LISTEN_CLIENT_URLS = concatStringsSep "," cfg.listenClientUrls;
         ETCD_LISTEN_PEER_URLS = concatStringsSep "," cfg.listenPeerUrls;
         ETCD_INITIAL_ADVERTISE_PEER_URLS = concatStringsSep "," cfg.initialAdvertisePeerUrls;
+        ETCD_PEER_CLIENT_CERT_AUTH = toString cfg.peerClientCertAuth;
         ETCD_PEER_TRUSTED_CA_FILE = cfg.peerTrustedCaFile;
         ETCD_PEER_CERT_FILE = cfg.peerCertFile;
         ETCD_PEER_KEY_FILE = cfg.peerKeyFile;
-        ETCD_CLIENT_CERT_AUTH = toString cfg.peerClientCertAuth;
+        ETCD_CLIENT_CERT_AUTH = toString cfg.clientCertAuth;
         ETCD_TRUSTED_CA_FILE = cfg.trustedCaFile;
         ETCD_CERT_FILE = cfg.certFile;
         ETCD_KEY_FILE = cfg.keyFile;
@@ -186,13 +189,13 @@ in {
 
       serviceConfig = {
         Type = "notify";
-        ExecStart = "${pkgs.etcd}/bin/etcd";
+        ExecStart = "${cfg.package}/bin/etcd";
         User = "etcd";
         LimitNOFILE = 40000;
       };
     };
 
-    environment.systemPackages = [ pkgs.etcd ];
+    environment.systemPackages = [ cfg.package ];
 
     users.users.etcd = {
       isSystemUser = true;
diff --git a/nixos/modules/services/misc/etebase-server.nix b/nixos/modules/services/misc/etebase-server.nix
index c3723d18814..045048a1a2e 100644
--- a/nixos/modules/services/misc/etebase-server.nix
+++ b/nixos/modules/services/misc/etebase-server.nix
@@ -179,21 +179,21 @@ in
       description = "An Etebase (EteSync 2.0) server";
       after = [ "network.target" "systemd-tmpfiles-setup.service" ];
       wantedBy = [ "multi-user.target" ];
+      path = [ pythonEnv ];
       serviceConfig = {
         User = cfg.user;
         Restart = "always";
         WorkingDirectory = cfg.dataDir;
       };
       environment = {
-        PYTHONPATH = "${pythonEnv}/${pkgs.python3.sitePackages}";
         ETEBASE_EASY_CONFIG_PATH = configIni;
       };
       preStart = ''
         # Auto-migrate on first run or if the package has changed
         versionFile="${cfg.dataDir}/src-version"
         if [[ $(cat "$versionFile" 2>/dev/null) != ${pkgs.etebase-server} ]]; then
-          ${pythonEnv}/bin/etebase-server migrate --no-input
-          ${pythonEnv}/bin/etebase-server collectstatic --no-input --clear
+          etebase-server migrate --no-input
+          etebase-server collectstatic --no-input --clear
           echo ${pkgs.etebase-server} > "$versionFile"
         fi
       '';
@@ -204,7 +204,7 @@ in
           else "-b 0.0.0.0 -p ${toString cfg.port}";
         in ''
           cd "${pythonEnv}/lib/etebase-server";
-          ${pythonEnv}/bin/daphne ${networking} \
+          daphne ${networking} \
             etebase_server.asgi:application
         '';
     };
diff --git a/nixos/modules/services/misc/exhibitor.nix b/nixos/modules/services/misc/exhibitor.nix
deleted file mode 100644
index 91a87b55af5..00000000000
--- a/nixos/modules/services/misc/exhibitor.nix
+++ /dev/null
@@ -1,417 +0,0 @@
-{ config, lib, options, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.exhibitor;
-  opt = options.services.exhibitor;
-  exhibitorConfig = ''
-    zookeeper-install-directory=${cfg.baseDir}/zookeeper
-    zookeeper-data-directory=${cfg.zkDataDir}
-    zookeeper-log-directory=${cfg.zkLogDir}
-    zoo-cfg-extra=${cfg.zkExtraCfg}
-    client-port=${toString cfg.zkClientPort}
-    connect-port=${toString cfg.zkConnectPort}
-    election-port=${toString cfg.zkElectionPort}
-    cleanup-period-ms=${toString cfg.zkCleanupPeriod}
-    servers-spec=${concatStringsSep "," cfg.zkServersSpec}
-    auto-manage-instances=${toString cfg.autoManageInstances}
-    ${cfg.extraConf}
-  '';
-  # NB: toString rather than lib.boolToString on cfg.autoManageInstances is intended.
-  # Exhibitor tests if it's an integer not equal to 0, so the empty string (toString false)
-  # will operate in the same fashion as a 0.
-  configDir = pkgs.writeTextDir "exhibitor.properties" exhibitorConfig;
-  cliOptionsCommon = {
-    configtype = cfg.configType;
-    defaultconfig = "${configDir}/exhibitor.properties";
-    port = toString cfg.port;
-    hostname = cfg.hostname;
-    headingtext = if (cfg.headingText != null) then (lib.escapeShellArg cfg.headingText) else null;
-    nodemodification = lib.boolToString cfg.nodeModification;
-    configcheckms = toString cfg.configCheckMs;
-    jquerystyle = cfg.jqueryStyle;
-    loglines = toString cfg.logLines;
-    servo = lib.boolToString cfg.servo;
-    timeout = toString cfg.timeout;
-  };
-  s3CommonOptions = { s3region = cfg.s3Region; s3credentials = cfg.s3Credentials; };
-  cliOptionsPerConfig = {
-    s3 = {
-      s3config = "${cfg.s3Config.bucketName}:${cfg.s3Config.objectKey}";
-      s3configprefix = cfg.s3Config.configPrefix;
-    };
-    zookeeper = {
-      zkconfigconnect = concatStringsSep "," cfg.zkConfigConnect;
-      zkconfigexhibitorpath = cfg.zkConfigExhibitorPath;
-      zkconfigpollms = toString cfg.zkConfigPollMs;
-      zkconfigretry = "${toString cfg.zkConfigRetry.sleepMs}:${toString cfg.zkConfigRetry.retryQuantity}";
-      zkconfigzpath = cfg.zkConfigZPath;
-      zkconfigexhibitorport = toString cfg.zkConfigExhibitorPort; # NB: This might be null
-    };
-    file = {
-      fsconfigdir = cfg.fsConfigDir;
-      fsconfiglockprefix = cfg.fsConfigLockPrefix;
-      fsConfigName = fsConfigName;
-    };
-    none = {
-      noneconfigdir = configDir;
-    };
-  };
-  cliOptions = concatStringsSep " " (mapAttrsToList (k: v: "--${k} ${v}") (filterAttrs (k: v: v != null && v != "") (cliOptionsCommon //
-               cliOptionsPerConfig.${cfg.configType} //
-               s3CommonOptions //
-               optionalAttrs cfg.s3Backup { s3backup = "true"; } //
-               optionalAttrs cfg.fileSystemBackup { filesystembackup = "true"; }
-               )));
-in
-{
-  options = {
-    services.exhibitor = {
-      enable = 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.port;
-        default = 8080;
-        description = lib.mdDoc ''
-          The port for exhibitor to listen on and communicate with other exhibitors.
-        '';
-      };
-      baseDir = mkOption {
-        type = types.str;
-        default = "/var/exhibitor";
-        description = lib.mdDoc ''
-          Baseline directory for exhibitor runtime config.
-        '';
-      };
-      configType = mkOption {
-        type = types.enum [ "file" "s3" "zookeeper" "none" ];
-        description = lib.mdDoc ''
-          Which configuration type you want to use. Additional config will be
-          required depending on which type you are using.
-        '';
-      };
-      hostname = mkOption {
-        type = types.nullOr types.str;
-        description = lib.mdDoc ''
-          Hostname to use and advertise
-        '';
-        default = null;
-      };
-      nodeModification = mkOption {
-        type = types.bool;
-        description = lib.mdDoc ''
-          Whether the Explorer UI will allow nodes to be modified (use with caution).
-        '';
-        default = true;
-      };
-      configCheckMs = mkOption {
-        type = types.int;
-        description = lib.mdDoc ''
-          Period (ms) to check for shared config updates.
-        '';
-        default = 30000;
-      };
-      headingText = mkOption {
-        type = types.nullOr types.str;
-        description = lib.mdDoc ''
-          Extra text to display in UI header
-        '';
-        default = null;
-      };
-      jqueryStyle = mkOption {
-        type = types.enum [ "red" "black" "custom" ];
-        description = lib.mdDoc ''
-          Styling used for the JQuery-based UI.
-        '';
-        default = "red";
-      };
-      logLines = mkOption {
-        type = types.int;
-        description = lib.mdDoc ''
-        Max lines of logging to keep in memory for display.
-        '';
-        default = 1000;
-      };
-      servo = mkOption {
-        type = types.bool;
-        description = lib.mdDoc ''
-          ZooKeeper will be queried once a minute for its state via the 'mntr' four
-          letter word (this requires ZooKeeper 3.4.x+). Servo will be used to publish
-          this data via JMX.
-        '';
-        default = false;
-      };
-      timeout = mkOption {
-        type = types.int;
-        description = lib.mdDoc ''
-          Connection timeout (ms) for ZK connections.
-        '';
-        default = 30000;
-      };
-      autoManageInstances = mkOption {
-        type = types.bool;
-        description = lib.mdDoc ''
-          Automatically manage ZooKeeper instances in the ensemble
-        '';
-        default = false;
-      };
-      zkDataDir = mkOption {
-        type = types.str;
-        default = "${cfg.baseDir}/zkData";
-        defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkData"'';
-        description = lib.mdDoc ''
-          The Zookeeper data directory
-        '';
-      };
-      zkLogDir = mkOption {
-        type = types.path;
-        default = "${cfg.baseDir}/zkLogs";
-        defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkLogs"'';
-        description = lib.mdDoc ''
-          The Zookeeper logs directory
-        '';
-      };
-      extraConf = mkOption {
-        type = types.str;
-        default = "";
-        description = lib.mdDoc ''
-          Extra Exhibitor configuration to put in the ZooKeeper config file.
-        '';
-      };
-      zkExtraCfg = mkOption {
-        type = types.str;
-        default = "initLimit=5&syncLimit=2&tickTime=2000";
-        description = lib.mdDoc ''
-          Extra options to pass into Zookeeper
-        '';
-      };
-      zkClientPort = mkOption {
-        type = types.int;
-        default = 2181;
-        description = lib.mdDoc ''
-          Zookeeper client port
-        '';
-      };
-      zkConnectPort = mkOption {
-        type = types.int;
-        default = 2888;
-        description = lib.mdDoc ''
-          The port to use for followers to talk to each other.
-        '';
-      };
-      zkElectionPort = mkOption {
-        type = types.int;
-        default = 3888;
-        description = lib.mdDoc ''
-          The port for Zookeepers to use for leader election.
-        '';
-      };
-      zkCleanupPeriod = mkOption {
-        type = types.int;
-        default = 0;
-        description = lib.mdDoc ''
-          How often (in milliseconds) to run the Zookeeper log cleanup task.
-        '';
-      };
-      zkServersSpec = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        description = lib.mdDoc ''
-          Zookeeper server spec for all servers in the ensemble.
-        '';
-        example = [ "S:1:zk1.example.com" "S:2:zk2.example.com" "S:3:zk3.example.com" "O:4:zk-observer.example.com" ];
-      };
-
-      # Backup options
-      s3Backup = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to enable backups to S3
-        '';
-      };
-      fileSystemBackup = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Enables file system backup of ZooKeeper log files
-        '';
-      };
-
-      # Options for using zookeeper configType
-      zkConfigConnect = mkOption {
-        type = types.listOf types.str;
-        description = lib.mdDoc ''
-          The initial connection string for ZooKeeper shared config storage
-        '';
-        example = ["host1:2181" "host2:2181"];
-      };
-      zkConfigExhibitorPath = mkOption {
-        type = types.str;
-        description = lib.mdDoc ''
-          If the ZooKeeper shared config is also running Exhibitor, the URI path for the REST call
-        '';
-        default = "/";
-      };
-      zkConfigExhibitorPort = mkOption {
-        type = types.nullOr types.int;
-        description = lib.mdDoc ''
-          If the ZooKeeper shared config is also running Exhibitor, the port that
-          Exhibitor is listening on. IMPORTANT: if this value is not set it implies
-          that Exhibitor is not being used on the ZooKeeper shared config.
-        '';
-      };
-      zkConfigPollMs = mkOption {
-        type = types.int;
-        description = lib.mdDoc ''
-          The period in ms to check for changes in the config ensemble
-        '';
-        default = 10000;
-      };
-      zkConfigRetry = {
-        sleepMs = mkOption {
-          type = types.int;
-          default = 1000;
-          description = lib.mdDoc ''
-            Retry sleep time connecting to the ZooKeeper config
-          '';
-        };
-        retryQuantity = mkOption {
-          type = types.int;
-          default = 3;
-          description = lib.mdDoc ''
-            Retries connecting to the ZooKeeper config
-          '';
-        };
-      };
-      zkConfigZPath = mkOption {
-        type = types.str;
-        description = lib.mdDoc ''
-          The base ZPath that Exhibitor should use
-        '';
-        example = "/exhibitor/config";
-      };
-
-      # Config options for s3 configType
-      s3Config = {
-        bucketName = mkOption {
-          type = types.str;
-          description = lib.mdDoc ''
-            Bucket name to store config
-          '';
-        };
-        objectKey = mkOption {
-          type = types.str;
-          description = lib.mdDoc ''
-            S3 key name to store the config
-          '';
-        };
-        configPrefix = mkOption {
-          type = types.str;
-          description = lib.mdDoc ''
-            When using AWS S3 shared config files, the prefix to use for values such as locks
-          '';
-          default = "exhibitor-";
-        };
-      };
-
-      # The next two are used for either s3backup or s3 configType
-      s3Credentials = mkOption {
-        type = types.nullOr types.path;
-        description = lib.mdDoc ''
-          Optional credentials to use for s3backup or s3config. Argument is the path
-          to an AWS credential properties file with two properties:
-          com.netflix.exhibitor.s3.access-key-id and com.netflix.exhibitor.s3.access-secret-key
-        '';
-        default = null;
-      };
-      s3Region = mkOption {
-        type = types.nullOr types.str;
-        description = lib.mdDoc ''
-          Optional region for S3 calls
-        '';
-        default = null;
-      };
-
-      # Config options for file config type
-      fsConfigDir = mkOption {
-        type = types.path;
-        description = lib.mdDoc ''
-          Directory to store Exhibitor properties (cannot be used with s3config).
-          Exhibitor uses file system locks so you can specify a shared location
-          so as to enable complete ensemble management.
-        '';
-      };
-      fsConfigLockPrefix = mkOption {
-        type = types.str;
-        description = lib.mdDoc ''
-          A prefix for a locking mechanism used in conjunction with fsconfigdir
-        '';
-        default = "exhibitor-lock-";
-      };
-      fsConfigName = mkOption {
-        type = types.str;
-        description = lib.mdDoc ''
-          The name of the file to store config in
-        '';
-        default = "exhibitor.properties";
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-    systemd.services.exhibitor = {
-      description = "Exhibitor Daemon";
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
-      environment = {
-        ZOO_LOG_DIR = cfg.baseDir;
-      };
-      serviceConfig = {
-        /***
-          Exhibitor is a bit un-nixy. It wants to present to you a user interface in order to
-          mutate the configuration of both itself and ZooKeeper, and to coordinate changes
-          among the members of the Zookeeper ensemble. I'm going for a different approach here,
-          which is to manage all the configuration via nix and have it write out the configuration
-          files that exhibitor will use, and to reduce the amount of inter-exhibitor orchestration.
-        ***/
-        ExecStart = ''
-          ${pkgs.exhibitor}/bin/startExhibitor.sh ${cliOptions}
-        '';
-        User = "zookeeper";
-        PermissionsStartOnly = true;
-      };
-      # This is a bit wonky, but the reason for this is that Exhibitor tries to write to
-      # ${cfg.baseDir}/zookeeper/bin/../conf/zoo.cfg
-      # I want everything but the conf directory to be in the immutable nix store, and I want defaults
-      # from the nix store
-      # If I symlink the bin directory in, then bin/../ will resolve to the parent of the symlink in the
-      # immutable nix store. Bind mounting a writable conf over the existing conf might work, but it gets very
-      # messy with trying to copy the existing out into a mutable store.
-      # Another option is to try to patch upstream exhibitor, but the current package just pulls down the
-      # prebuild JARs off of Maven, rather than building them ourselves, as Maven support in Nix isn't
-      # very mature. So, it seems like a reasonable compromise is to just copy out of the immutable store
-      # just before starting the service, so we're running binaries from the immutable store, but we work around
-      # Exhibitor's desire to mutate its current installation.
-      preStart = ''
-        mkdir -m 0700 -p ${cfg.baseDir}/zookeeper
-        # Not doing a chown -R to keep the base ZK files owned by root
-        chown zookeeper ${cfg.baseDir} ${cfg.baseDir}/zookeeper
-        cp -Rf ${pkgs.zookeeper}/* ${cfg.baseDir}/zookeeper
-        chown -R zookeeper ${cfg.baseDir}/zookeeper/conf
-        chmod -R u+w ${cfg.baseDir}/zookeeper/conf
-        replace_what=$(echo ${pkgs.zookeeper} | sed 's/[\/&]/\\&/g')
-        replace_with=$(echo ${cfg.baseDir}/zookeeper | sed 's/[\/&]/\\&/g')
-        sed -i 's/'"$replace_what"'/'"$replace_with"'/g' ${cfg.baseDir}/zookeeper/bin/zk*.sh
-      '';
-    };
-    users.users.zookeeper = {
-      uid = config.ids.uids.zookeeper;
-      description = "Zookeeper daemon user";
-      home = cfg.baseDir;
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/fstrim.nix b/nixos/modules/services/misc/fstrim.nix
index 36b5f9c8cca..55fb24e2927 100644
--- a/nixos/modules/services/misc/fstrim.nix
+++ b/nixos/modules/services/misc/fstrim.nix
@@ -34,7 +34,7 @@ in {
 
     systemd.timers.fstrim = {
       timerConfig = {
-        OnCalendar = cfg.interval;
+        OnCalendar = [ "" cfg.interval ];
       };
       wantedBy = [ "timers.target" ];
     };
diff --git a/nixos/modules/services/misc/gammu-smsd.nix b/nixos/modules/services/misc/gammu-smsd.nix
index 83f4efe695a..eff725f5a86 100644
--- a/nixos/modules/services/misc/gammu-smsd.nix
+++ b/nixos/modules/services/misc/gammu-smsd.nix
@@ -10,7 +10,7 @@ let
     Connection = ${cfg.device.connection}
     SynchronizeTime = ${if cfg.device.synchronizeTime then "yes" else "no"}
     LogFormat = ${cfg.log.format}
-    ${if (cfg.device.pin != null) then "PIN = ${cfg.device.pin}" else ""}
+    ${optionalString (cfg.device.pin != null) "PIN = ${cfg.device.pin}"}
     ${cfg.extraConfig.gammu}
 
 
@@ -33,10 +33,10 @@ let
     ${optionalString (cfg.backend.service == "sql" && cfg.backend.sql.driver == "native_pgsql") (
       with cfg.backend; ''
         Driver = ${sql.driver}
-        ${if (sql.database!= null) then "Database = ${sql.database}" else ""}
-        ${if (sql.host != null) then "Host = ${sql.host}" else ""}
-        ${if (sql.user != null) then "User = ${sql.user}" else ""}
-        ${if (sql.password != null) then "Password = ${sql.password}" else ""}
+        ${optionalString (sql.database!= null) "Database = ${sql.database}"}
+        ${optionalString (sql.host != null) "Host = ${sql.host}"}
+        ${optionalString (sql.user != null) "User = ${sql.user}"}
+        ${optionalString (sql.password != null) "Password = ${sql.password}"}
       '')}
 
     ${cfg.extraConfig.smsd}
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index 00e90f5b32b..945009f0058 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -5,7 +5,7 @@ with lib;
 let
   cfg = config.services.gitea;
   opt = options.services.gitea;
-  gitea = cfg.package;
+  exe = lib.getExe cfg.package;
   pg = config.services.postgresql;
   useMysql = cfg.database.type == "mysql";
   usePostgresql = cfg.database.type == "postgres";
@@ -26,9 +26,18 @@ in
   imports = [
     (mkRenamedOptionModule [ "services" "gitea" "cookieSecure" ] [ "services" "gitea" "settings" "session" "COOKIE_SECURE" ])
     (mkRenamedOptionModule [ "services" "gitea" "disableRegistration" ] [ "services" "gitea" "settings" "service" "DISABLE_REGISTRATION" ])
+    (mkRenamedOptionModule [ "services" "gitea" "domain" ] [ "services" "gitea" "settings" "server" "DOMAIN" ])
+    (mkRenamedOptionModule [ "services" "gitea" "httpAddress" ] [ "services" "gitea" "settings" "server" "HTTP_ADDR" ])
+    (mkRenamedOptionModule [ "services" "gitea" "httpPort" ] [ "services" "gitea" "settings" "server" "HTTP_PORT" ])
     (mkRenamedOptionModule [ "services" "gitea" "log" "level" ] [ "services" "gitea" "settings" "log" "LEVEL" ])
     (mkRenamedOptionModule [ "services" "gitea" "log" "rootPath" ] [ "services" "gitea" "settings" "log" "ROOT_PATH" ])
+    (mkRenamedOptionModule [ "services" "gitea" "rootUrl" ] [ "services" "gitea" "settings" "server" "ROOT_URL" ])
     (mkRenamedOptionModule [ "services" "gitea" "ssh" "clonePort" ] [ "services" "gitea" "settings" "server" "SSH_PORT" ])
+    (mkRenamedOptionModule [ "services" "gitea" "staticRootPath" ] [ "services" "gitea" "settings" "server" "STATIC_ROOT_PATH" ])
+
+    (mkChangedOptionModule [ "services" "gitea" "enableUnixSocket" ] [ "services" "gitea" "settings" "server" "PROTOCOL" ] (
+      config: if config.services.gitea.enableUnixSocket then "http+unix" else "http"
+    ))
 
     (mkRemovedOptionModule [ "services" "gitea" "ssh" "enable" ] "services.gitea.ssh.enable has been migrated into freeform setting services.gitea.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted")
   ];
@@ -57,7 +66,14 @@ in
       stateDir = mkOption {
         default = "/var/lib/gitea";
         type = types.str;
-        description = lib.mdDoc "gitea data directory.";
+        description = lib.mdDoc "Gitea data directory.";
+      };
+
+      customDir = mkOption {
+        default = "${cfg.stateDir}/custom";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/custom"'';
+        type = types.str;
+        description = lib.mdDoc "Gitea custom directory. Used for config, custom templates and other options.";
       };
 
       user = mkOption {
@@ -66,6 +82,12 @@ in
         description = lib.mdDoc "User account under which gitea runs.";
       };
 
+      group = mkOption {
+        type = types.str;
+        default = "gitea";
+        description = lib.mdDoc "Group under which gitea runs.";
+      };
+
       database = {
         type = mkOption {
           type = types.enum [ "sqlite3" "mysql" "postgres" ];
@@ -175,7 +197,7 @@ in
         };
 
         type = mkOption {
-          type = types.enum [ "zip" "rar" "tar" "sz" "tar.gz" "tar.xz" "tar.bz2" "tar.br" "tar.lz4" ];
+          type = types.enum [ "zip" "rar" "tar" "sz" "tar.gz" "tar.xz" "tar.bz2" "tar.br" "tar.lz4" "tar.zst" ];
           default = "zip";
           description = lib.mdDoc "Archive format used to store the dump file.";
         };
@@ -216,44 +238,6 @@ in
         description = lib.mdDoc "Path to the git repositories.";
       };
 
-      domain = mkOption {
-        type = types.str;
-        default = "localhost";
-        description = lib.mdDoc "Domain name of your server.";
-      };
-
-      rootUrl = mkOption {
-        type = types.str;
-        default = "http://localhost:3000/";
-        description = lib.mdDoc "Full public URL of gitea server.";
-      };
-
-      httpAddress = mkOption {
-        type = types.str;
-        default = "0.0.0.0";
-        description = lib.mdDoc "HTTP listen address.";
-      };
-
-      httpPort = mkOption {
-        type = types.port;
-        default = 3000;
-        description = lib.mdDoc "HTTP listen port.";
-      };
-
-      enableUnixSocket = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Configure Gitea to listen on a unix socket instead of the default TCP port.";
-      };
-
-      staticRootPath = mkOption {
-        type = types.either types.str types.path;
-        default = gitea.data;
-        defaultText = literalExpression "package.data";
-        example = "/var/lib/gitea/data";
-        description = lib.mdDoc "Upper level of template and static files path.";
-      };
-
       mailerPasswordFile = mkOption {
         type = types.nullOr types.str;
         default = null;
@@ -285,7 +269,7 @@ in
             };
           }
         '';
-        type = with types; submodule {
+        type = types.submodule {
           freeformType = format.type;
           options = {
             log = {
@@ -303,6 +287,46 @@ in
             };
 
             server = {
+              PROTOCOL = mkOption {
+                type = types.enum [ "http" "https" "fcgi" "http+unix" "fcgi+unix" ];
+                default = "http";
+                description = lib.mdDoc ''Listen protocol. `+unix` means "over unix", not "in addition to."'';
+              };
+
+              HTTP_ADDR = mkOption {
+                type = types.either types.str types.path;
+                default = if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/gitea/gitea.sock" else "0.0.0.0";
+                defaultText = literalExpression ''if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/gitea/gitea.sock" else "0.0.0.0"'';
+                description = lib.mdDoc "Listen address. Must be a path when using a unix socket.";
+              };
+
+              HTTP_PORT = mkOption {
+                type = types.port;
+                default = 3000;
+                description = lib.mdDoc "Listen port. Ignored when using a unix socket.";
+              };
+
+              DOMAIN = mkOption {
+                type = types.str;
+                default = "localhost";
+                description = lib.mdDoc "Domain name of your server.";
+              };
+
+              ROOT_URL = mkOption {
+                type = types.str;
+                default = "http://${cfg.settings.server.DOMAIN}:${toString cfg.settings.server.HTTP_PORT}/";
+                defaultText = literalExpression ''"http://''${config.services.gitea.settings.server.DOMAIN}:''${toString config.services.gitea.settings.server.HTTP_PORT}/"'';
+                description = lib.mdDoc "Full public URL of gitea server.";
+              };
+
+              STATIC_ROOT_PATH = mkOption {
+                type = types.either types.str types.path;
+                default = cfg.package.data;
+                defaultText = literalExpression "config.${opt.package}.data";
+                example = "/var/lib/gitea/data";
+                description = lib.mdDoc "Upper level of template and static files path.";
+              };
+
               DISABLE_SSH = mkOption {
                 type = types.bool;
                 default = false;
@@ -359,12 +383,14 @@ in
 
   config = mkIf cfg.enable {
     assertions = [
-      { assertion = cfg.database.createDatabase -> cfg.database.user == cfg.user;
+      { assertion = cfg.database.createDatabase -> useSqlite || cfg.database.user == cfg.user;
         message = "services.gitea.database.user must match services.gitea.user if the database is to be automatically provisioned";
       }
     ];
 
     services.gitea.settings = {
+      "cron.update_checker".ENABLED = lib.mkDefault false;
+
       database = mkMerge [
         {
           DB_TYPE = cfg.database.type;
@@ -387,26 +413,10 @@ in
         ROOT = cfg.repositoryRoot;
       };
 
-      server = mkMerge [
-        {
-          DOMAIN = cfg.domain;
-          STATIC_ROOT_PATH = toString cfg.staticRootPath;
-          LFS_JWT_SECRET = "#lfsjwtsecret#";
-          ROOT_URL = cfg.rootUrl;
-        }
-        (mkIf cfg.enableUnixSocket {
-          PROTOCOL = "http+unix";
-          HTTP_ADDR = "/run/gitea/gitea.sock";
-        })
-        (mkIf (!cfg.enableUnixSocket) {
-          HTTP_ADDR = cfg.httpAddress;
-          HTTP_PORT = cfg.httpPort;
-        })
-        (mkIf cfg.lfs.enable {
-          LFS_START_SERVER = true;
-        })
-
-      ];
+      server = mkIf cfg.lfs.enable {
+        LFS_START_SERVER = true;
+        LFS_JWT_SECRET = "#lfsjwtsecret#";
+      };
 
       session = {
         COOKIE_NAME = lib.mkDefault "session";
@@ -426,7 +436,7 @@ in
         JWT_SECRET = "#oauth2jwtsecret#";
       };
 
-      lfs = mkIf (cfg.lfs.enable) {
+      lfs = mkIf cfg.lfs.enable {
         PATH = cfg.lfs.contentDir;
       };
     };
@@ -455,38 +465,39 @@ in
     };
 
     systemd.tmpfiles.rules = [
-      "d '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
-      "Z '${cfg.dump.backupDir}' - ${cfg.user} gitea - -"
-      "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
-      "Z '${cfg.lfs.contentDir}' - ${cfg.user} gitea - -"
-      "d '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
-      "Z '${cfg.repositoryRoot}' - ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
-      "Z '${cfg.stateDir}' - ${cfg.user} gitea - -"
+      "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
 
       # If we have a folder or symlink with gitea locales, remove it
       # And symlink the current gitea locales in place
-      "L+ '${cfg.stateDir}/conf/locale' - - - - ${gitea.out}/locale"
+      "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale"
+
+    ] ++ lib.optionals cfg.lfs.enable [
+      "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
     ];
 
     systemd.services.gitea = {
       description = "gitea";
-      after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
+      after = [ "network.target" ] ++ optional usePostgresql "postgresql.service" ++ optional useMysql "mysql.service";
+      requires = optional (cfg.database.createDatabase && usePostgresql) "postgresql.service" ++ optional (cfg.database.createDatabase && useMysql) "mysql.service";
       wantedBy = [ "multi-user.target" ];
-      path = [ gitea pkgs.git pkgs.gnupg ];
+      path = [ cfg.package pkgs.git pkgs.gnupg ];
 
       # In older versions the secret naming for JWT was kind of confusing.
       # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET
@@ -496,47 +507,52 @@ in
       # lfs_jwt_secret.
       # We have to consider this to stay compatible with older installations.
       preStart = let
-        runConfig = "${cfg.stateDir}/custom/conf/app.ini";
-        secretKey = "${cfg.stateDir}/custom/conf/secret_key";
-        oauth2JwtSecret = "${cfg.stateDir}/custom/conf/oauth2_jwt_secret";
-        oldLfsJwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret"; # old file for LFS_JWT_SECRET
-        lfsJwtSecret = "${cfg.stateDir}/custom/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET
-        internalToken = "${cfg.stateDir}/custom/conf/internal_token";
+        runConfig = "${cfg.customDir}/conf/app.ini";
+        secretKey = "${cfg.customDir}/conf/secret_key";
+        oauth2JwtSecret = "${cfg.customDir}/conf/oauth2_jwt_secret";
+        oldLfsJwtSecret = "${cfg.customDir}/conf/jwt_secret"; # old file for LFS_JWT_SECRET
+        lfsJwtSecret = "${cfg.customDir}/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET
+        internalToken = "${cfg.customDir}/conf/internal_token";
         replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret";
       in ''
-        # copy custom configuration and generate a random secret key if needed
+        # copy custom configuration and generate random secrets if needed
         ${optionalString (!cfg.useWizard) ''
           function gitea_setup {
-            cp -f ${configFile} ${runConfig}
+            cp -f '${configFile}' '${runConfig}'
 
-            if [ ! -s ${secretKey} ]; then
-                ${gitea}/bin/gitea generate secret SECRET_KEY > ${secretKey}
+            if [ ! -s '${secretKey}' ]; then
+                ${exe} generate secret SECRET_KEY > '${secretKey}'
             fi
 
             # Migrate LFS_JWT_SECRET filename
-            if [[ -s ${oldLfsJwtSecret} && ! -s ${lfsJwtSecret} ]]; then
-                mv ${oldLfsJwtSecret} ${lfsJwtSecret}
+            if [[ -s '${oldLfsJwtSecret}' && ! -s '${lfsJwtSecret}' ]]; then
+                mv '${oldLfsJwtSecret}' '${lfsJwtSecret}'
             fi
 
-            if [ ! -s ${oauth2JwtSecret} ]; then
-                ${gitea}/bin/gitea generate secret JWT_SECRET > ${oauth2JwtSecret}
+            if [ ! -s '${oauth2JwtSecret}' ]; then
+                ${exe} generate secret JWT_SECRET > '${oauth2JwtSecret}'
             fi
 
-            if [ ! -s ${lfsJwtSecret} ]; then
-                ${gitea}/bin/gitea generate secret LFS_JWT_SECRET > ${lfsJwtSecret}
+            ${lib.optionalString cfg.lfs.enable ''
+            if [ ! -s '${lfsJwtSecret}' ]; then
+                ${exe} generate secret LFS_JWT_SECRET > '${lfsJwtSecret}'
             fi
+            ''}
 
-            if [ ! -s ${internalToken} ]; then
-                ${gitea}/bin/gitea generate secret INTERNAL_TOKEN > ${internalToken}
+            if [ ! -s '${internalToken}' ]; then
+                ${exe} generate secret INTERNAL_TOKEN > '${internalToken}'
             fi
 
             chmod u+w '${runConfig}'
             ${replaceSecretBin} '#secretkey#' '${secretKey}' '${runConfig}'
             ${replaceSecretBin} '#dbpass#' '${cfg.database.passwordFile}' '${runConfig}'
             ${replaceSecretBin} '#oauth2jwtsecret#' '${oauth2JwtSecret}' '${runConfig}'
-            ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}'
             ${replaceSecretBin} '#internaltoken#' '${internalToken}' '${runConfig}'
 
+            ${lib.optionalString cfg.lfs.enable ''
+              ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}'
+            ''}
+
             ${lib.optionalString (cfg.mailerPasswordFile != null) ''
               ${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}'
             ''}
@@ -546,30 +562,33 @@ in
         ''}
 
         # run migrations/init the database
-        ${gitea}/bin/gitea migrate
+        ${exe} migrate
 
         # update all hooks' binary paths
-        ${gitea}/bin/gitea admin regenerate hooks
+        ${exe} admin regenerate hooks
 
         # update command option in authorized_keys
         if [ -r ${cfg.stateDir}/.ssh/authorized_keys ]
         then
-          ${gitea}/bin/gitea admin regenerate keys
+          ${exe} admin regenerate keys
         fi
       '';
 
       serviceConfig = {
         Type = "simple";
         User = cfg.user;
-        Group = "gitea";
+        Group = cfg.group;
         WorkingDirectory = cfg.stateDir;
-        ExecStart = "${gitea}/bin/gitea web --pid /run/gitea/gitea.pid";
+        ExecStart = "${exe} web --pid /run/gitea/gitea.pid";
         Restart = "always";
         # Runtime directory and mode
         RuntimeDirectory = "gitea";
-        RuntimeDirectoryMode = "0755";
+        RuntimeDirectoryMode = "0750";
+        # Proc filesystem
+        ProcSubset = "pid";
+        ProtectProc = "invisible";
         # Access write directories
-        ReadWritePaths = [ cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ];
+        ReadWritePaths = [ cfg.customDir cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ];
         UMask = "0027";
         # Capabilities
         CapabilityBoundingSet = "";
@@ -587,21 +606,24 @@ in
         ProtectKernelModules = true;
         ProtectKernelLogs = true;
         ProtectControlGroups = true;
-        RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ];
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
         LockPersonality = true;
         MemoryDenyWriteExecute = true;
         RestrictRealtime = true;
         RestrictSUIDSGID = true;
+        RemoveIPC = true;
         PrivateMounts = true;
         # System Call Filtering
         SystemCallArchitectures = "native";
-        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @setuid @swap";
+        SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" "setrlimit" ];
       };
 
       environment = {
         USER = cfg.user;
         HOME = cfg.stateDir;
         GITEA_WORK_DIR = cfg.stateDir;
+        GITEA_CUSTOM = cfg.customDir;
       };
     };
 
@@ -610,12 +632,14 @@ in
         description = "Gitea Service";
         home = cfg.stateDir;
         useDefaultShell = true;
-        group = "gitea";
+        group = cfg.group;
         isSystemUser = true;
       };
     };
 
-    users.groups.gitea = {};
+    users.groups = mkIf (cfg.group == "gitea") {
+      gitea = {};
+    };
 
     warnings =
       optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++
@@ -633,8 +657,7 @@ in
     systemd.services.gitea-dump = mkIf cfg.dump.enable {
        description = "gitea dump";
        after = [ "gitea.service" ];
-       wantedBy = [ "default.target" ];
-       path = [ gitea ];
+       path = [ cfg.package ];
 
        environment = {
          USER = cfg.user;
@@ -645,7 +668,7 @@ in
        serviceConfig = {
          Type = "oneshot";
          User = cfg.user;
-         ExecStart = "${gitea}/bin/gitea dump --type ${cfg.dump.type}" + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}";
+         ExecStart = "${exe} dump --type ${cfg.dump.type}" + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}";
          WorkingDirectory = cfg.dump.backupDir;
        };
     };
@@ -657,5 +680,5 @@ in
       timerConfig.OnCalendar = cfg.dump.interval;
     };
   };
-  meta.maintainers = with lib.maintainers; [ srhb ma27 ];
+  meta.maintainers = with lib.maintainers; [ srhb ma27 thehedgeh0g ];
 }
diff --git a/nixos/modules/services/misc/gitit.nix b/nixos/modules/services/misc/gitit.nix
deleted file mode 100644
index 0fafa76b548..00000000000
--- a/nixos/modules/services/misc/gitit.nix
+++ /dev/null
@@ -1,725 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.gitit;
-
-  homeDir = "/var/lib/gitit";
-
-  toYesNo = b: if b then "yes" else "no";
-
-  gititShared = with cfg.haskellPackages; gitit + "/share/" + ghc.targetPrefix + ghc.haskellCompilerName + "/" + gitit.pname + "-" + gitit.version;
-
-  gititWithPkgs = hsPkgs: extras: hsPkgs.ghcWithPackages (self: with self; [ gitit ] ++ (extras self));
-
-  gititSh = hsPkgs: extras: with pkgs; let
-    env = gititWithPkgs hsPkgs extras;
-  in writeScript "gitit" ''
-    #!${runtimeShell}
-    cd $HOME
-    export NIX_GHC="${env}/bin/ghc"
-    export NIX_GHCPKG="${env}/bin/ghc-pkg"
-    export NIX_GHC_DOCDIR="${env}/share/doc/ghc/html"
-    export NIX_GHC_LIBDIR=$( $NIX_GHC --print-libdir )
-    ${env}/bin/gitit -f ${configFile}
-  '';
-
-  gititOptions = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Enable the gitit service.";
-      };
-
-      haskellPackages = mkOption {
-        default = pkgs.haskellPackages;
-        defaultText = literalExpression "pkgs.haskellPackages";
-        example = literalExpression "pkgs.haskell.packages.ghc784";
-        description = lib.mdDoc "haskellPackages used to build gitit and plugins.";
-      };
-
-      extraPackages = mkOption {
-        type = types.functionTo (types.listOf types.package);
-        default = self: [];
-        example = literalExpression ''
-          haskellPackages: [
-            haskellPackages.wreq
-          ]
-        '';
-        description = lib.mdDoc ''
-          Extra packages available to ghc when running gitit. The
-          value must be a function which receives the attrset defined
-          in {var}`haskellPackages` as the sole argument.
-        '';
-      };
-
-      address = mkOption {
-        type = types.str;
-        default = "0.0.0.0";
-        description = lib.mdDoc "IP address on which the web server will listen.";
-      };
-
-      port = mkOption {
-        type = types.int;
-        default = 5001;
-        description = lib.mdDoc "Port on which the web server will run.";
-      };
-
-      wikiTitle = mkOption {
-        type = types.str;
-        default = "Gitit!";
-        description = lib.mdDoc "The wiki title.";
-      };
-
-      repositoryType = mkOption {
-        type = types.enum ["git" "darcs" "mercurial"];
-        default = "git";
-        description = lib.mdDoc "Specifies the type of repository used for wiki content.";
-      };
-
-      repositoryPath = mkOption {
-        type = types.path;
-        default = homeDir + "/wiki";
-        description = lib.mdDoc ''
-          Specifies the path of the repository directory. If it does not
-          exist, gitit will create it on startup.
-        '';
-      };
-
-      requireAuthentication = mkOption {
-        type = types.enum [ "none" "modify" "read" ];
-        default = "modify";
-        description = lib.mdDoc ''
-          If 'none', login is never required, and pages can be edited
-          anonymously.  If 'modify', login is required to modify the wiki
-          (edit, add, delete pages, upload files).  If 'read', login is
-          required to see any wiki pages.
-        '';
-      };
-
-      authenticationMethod = mkOption {
-        type = types.enum [ "form" "http" "generic" "github" ];
-        default = "form";
-        description = lib.mdDoc ''
-          'form' means that users will be logged in and registered using forms
-          in the gitit web interface.  'http' means that gitit will assume that
-          HTTP authentication is in place and take the logged in username from
-          the "Authorization" field of the HTTP request header (in addition,
-          the login/logout and registration links will be suppressed).
-          'generic' means that gitit will assume that some form of
-          authentication is in place that directly sets REMOTE_USER to the name
-          of the authenticated user (e.g. mod_auth_cas on apache).  'rpx' means
-          that gitit will attempt to log in through https://rpxnow.com.  This
-          requires that 'rpx-domain', 'rpx-key', and 'base-url' be set below,
-          and that 'curl' be in the system path.
-        '';
-      };
-
-      userFile = mkOption {
-        type = types.path;
-        default = homeDir + "/gitit-users";
-        description = lib.mdDoc ''
-          Specifies the path of the file containing user login information.  If
-          it does not exist, gitit will create it (with an empty user list).
-          This file is not used if 'http' is selected for
-          authentication-method.
-        '';
-      };
-
-      sessionTimeout = mkOption {
-        type = types.int;
-        default = 60;
-        description = lib.mdDoc ''
-          Number of minutes of inactivity before a session expires.
-        '';
-      };
-
-      staticDir = mkOption {
-        type = types.path;
-        default = gititShared + "/data/static";
-        description = lib.mdDoc ''
-          Specifies the path of the static directory (containing javascript,
-          css, and images).  If it does not exist, gitit will create it and
-          populate it with required scripts, stylesheets, and images.
-        '';
-      };
-
-      defaultPageType = mkOption {
-        type = types.enum [ "markdown" "rst" "latex" "html" "markdown+lhs" "rst+lhs" "latex+lhs" ];
-        default = "markdown";
-        description = lib.mdDoc ''
-          Specifies the type of markup used to interpret pages in the wiki.
-          Possible values are markdown, rst, latex, html, markdown+lhs,
-          rst+lhs, and latex+lhs. (the +lhs variants treat the input as
-          literate Haskell. See pandoc's documentation for more details.) If
-          Markdown is selected, pandoc's syntax extensions (for footnotes,
-          delimited code blocks, etc.) will be enabled. Note that pandoc's
-          restructuredtext parser is not complete, so some pages may not be
-          rendered correctly if rst is selected. The same goes for latex and
-          html.
-        '';
-      };
-
-      math = mkOption {
-        type = types.enum [ "mathml" "raw" "mathjax" "jsmath" "google" ];
-        default = "mathml";
-        description = lib.mdDoc ''
-          Specifies how LaTeX math is to be displayed.  Possible values are
-          mathml, raw, mathjax, jsmath, and google.  If mathml is selected,
-          gitit will convert LaTeX math to MathML and link in a script,
-          MathMLinHTML.js, that allows the MathML to be seen in Gecko browsers,
-          IE + mathplayer, and Opera. In other browsers you may get a jumble of
-          characters.  If raw is selected, the LaTeX math will be displayed as
-          raw LaTeX math.  If mathjax is selected, gitit will link to the
-          remote mathjax script.  If jsMath is selected, gitit will link to the
-          script /js/jsMath/easy/load.js, and will assume that jsMath has been
-          installed into the js/jsMath directory.  This is the most portable
-          solution. If google is selected, the google chart API is called to
-          render the formula as an image. This requires a connection to google,
-          and might raise a technical or a privacy problem.
-        '';
-      };
-
-      mathJaxScript = mkOption {
-        type = types.str;
-        default = "https://d3eoax9i5htok0.cloudfront.net/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
-        description = lib.mdDoc ''
-          Specifies the path to MathJax rendering script.  You might want to
-          use your own MathJax script to render formulas without Internet
-          connection or if you want to use some special LaTeX packages.  Note:
-          path specified there cannot be an absolute path to a script on your
-          hdd, instead you should run your (local if you wish) HTTP server
-          which will serve the MathJax.js script. You can easily (in four lines
-          of code) serve MathJax.js using
-          http://happstack.com/docs/crashcourse/FileServing.html Do not forget
-          the "http://" prefix (e.g. http://localhost:1234/MathJax.js).
-        '';
-      };
-
-      showLhsBirdTracks = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Specifies whether to show Haskell code blocks in "bird style", with
-          "> " at the beginning of each line.
-        '';
-      };
-
-      templatesDir = mkOption {
-        type = types.path;
-        default = gititShared + "/data/templates";
-        description = lib.mdDoc ''
-          Specifies the path of the directory containing page templates.  If it
-          does not exist, gitit will create it with default templates.  Users
-          may wish to edit the templates to customize the appearance of their
-          wiki. The template files are HStringTemplate templates.  Variables to
-          be interpolated appear between $\'s. Literal $\'s must be
-          backslash-escaped.
-        '';
-      };
-
-      logFile = mkOption {
-        type = types.path;
-        default = homeDir + "/gitit.log";
-        description = lib.mdDoc ''
-          Specifies the path of gitit's log file.  If it does not exist, gitit
-          will create it. The log is in Apache combined log format.
-        '';
-      };
-
-      logLevel = mkOption {
-        type = types.enum [ "DEBUG" "INFO" "NOTICE" "WARNING" "ERROR" "CRITICAL" "ALERT" "EMERGENCY" ];
-        default = "ERROR";
-        description = lib.mdDoc ''
-          Determines how much information is logged.  Possible values (from
-          most to least verbose) are DEBUG, INFO, NOTICE, WARNING, ERROR,
-          CRITICAL, ALERT, EMERGENCY.
-        '';
-      };
-
-      frontPage = mkOption {
-        type = types.str;
-        default = "Front Page";
-        description = lib.mdDoc ''
-          Specifies which wiki page is to be used as the wiki's front page.
-          Gitit creates a default front page on startup, if one does not exist
-          already.
-        '';
-      };
-
-      noDelete = mkOption {
-        type = types.str;
-        default = "Front Page, Help";
-        description = lib.mdDoc ''
-          Specifies pages that cannot be deleted through the web interface.
-          (They can still be deleted directly using git or darcs.) A
-          comma-separated list of page names.  Leave blank to allow every page
-          to be deleted.
-        '';
-      };
-
-      noEdit = mkOption {
-        type = types.str;
-        default = "Help";
-        description = lib.mdDoc ''
-          Specifies pages that cannot be edited through the web interface.
-          Leave blank to allow every page to be edited.
-        '';
-      };
-
-      defaultSummary = mkOption {
-        type = types.str;
-        default = "";
-        description = lib.mdDoc ''
-          Specifies text to be used in the change description if the author
-          leaves the "description" field blank.  If default-summary is blank
-          (the default), the author will be required to fill in the description
-          field.
-        '';
-      };
-
-      tableOfContents = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Specifies whether to print a tables of contents (with links to
-          sections) on each wiki page.
-        '';
-      };
-
-      plugins = mkOption {
-        type = with types; listOf str;
-        default = [ (gititShared + "/plugins/Dot.hs") ];
-        description = lib.mdDoc ''
-          Specifies a list of plugins to load. Plugins may be specified either
-          by their path or by their module name. If the plugin name starts
-          with Gitit.Plugin., gitit will assume that the plugin is an installed
-          module and will not try to find a source file.
-        '';
-      };
-
-      useCache = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Specifies whether to cache rendered pages.  Note that if use-feed is
-          selected, feeds will be cached regardless of the value of use-cache.
-        '';
-      };
-
-      cacheDir = mkOption {
-        type = types.path;
-        default = homeDir + "/cache";
-        description = lib.mdDoc "Path where rendered pages will be cached.";
-      };
-
-      maxUploadSize = mkOption {
-        type = types.str;
-        default = "1000K";
-        description = lib.mdDoc ''
-          Specifies an upper limit on the size (in bytes) of files uploaded
-          through the wiki's web interface.  To disable uploads, set this to
-          0K.  This will result in the uploads link disappearing and the
-          _upload url becoming inactive.
-        '';
-      };
-
-      maxPageSize = mkOption {
-        type = types.str;
-        default = "1000K";
-        description = lib.mdDoc "Specifies an upper limit on the size (in bytes) of pages.";
-      };
-
-      debugMode = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Causes debug information to be logged while gitit is running.";
-      };
-
-      compressResponses = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc "Specifies whether HTTP responses should be compressed.";
-      };
-
-      mimeTypesFile = mkOption {
-        type = types.path;
-        default = "/etc/mime/types.info";
-        description = lib.mdDoc ''
-          Specifies the path of a file containing mime type mappings.  Each
-          line of the file should contain two fields, separated by whitespace.
-          The first field is the mime type, the second is a file extension.
-          For example:
-          ```
-          video/x-ms-wmx  wmx
-          ```
-          If the file is not found, some simple defaults will be used.
-        '';
-      };
-
-      useReCaptcha = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          If true, causes gitit to use the reCAPTCHA service
-          (http://recaptcha.net) to prevent bots from creating accounts.
-        '';
-      };
-
-      reCaptchaPrivateKey = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          Specifies the private key for the reCAPTCHA service.  To get
-          these, you need to create an account at http://recaptcha.net.
-        '';
-      };
-
-      reCaptchaPublicKey = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          Specifies the public key for the reCAPTCHA service.  To get
-          these, you need to create an account at http://recaptcha.net.
-        '';
-      };
-
-      accessQuestion = mkOption {
-        type = types.str;
-        default = "What is the code given to you by Ms. X?";
-        description = lib.mdDoc ''
-          Specifies a question that users must answer when they attempt to
-          create an account
-        '';
-      };
-
-      accessQuestionAnswers = mkOption {
-        type = types.str;
-        default = "RED DOG, red dog";
-        description = lib.mdDoc ''
-          Specifies a question that users must answer when they attempt to
-          create an account, along with a comma-separated list of acceptable
-          answers.  This can be used to institute a rudimentary password for
-          signing up as a user on the wiki, or as an alternative to reCAPTCHA.
-          Example:
-          access-question:  What is the code given to you by Ms. X?
-          access-question-answers:  RED DOG, red dog
-        '';
-      };
-
-      rpxDomain = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          Specifies the domain and key of your RPX account.  The domain is just
-          the prefix of the complete RPX domain, so if your full domain is
-          'https://foo.rpxnow.com/', use 'foo' as the value of rpx-domain.
-        '';
-      };
-
-      rpxKey = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "RPX account access key.";
-      };
-
-      mailCommand = mkOption {
-        type = types.str;
-        default = "sendmail %s";
-        description = lib.mdDoc ''
-          Specifies the command to use to send notification emails.  '%s' will
-          be replaced by the destination email address.  The body of the
-          message will be read from stdin.  If this field is left blank,
-          password reset will not be offered.
-        '';
-      };
-
-      resetPasswordMessage = mkOption {
-        type = types.lines;
-        default = ''
-          > From: gitit@$hostname$
-          > To: $useremail$
-          > Subject: Wiki password reset
-          >
-          > Hello $username$,
-          >
-          > To reset your password, please follow the link below:
-          > http://$hostname$:$port$$resetlink$
-          >
-          > Regards
-        '';
-        description = lib.mdDoc ''
-          Gives the text of the message that will be sent to the user should
-          she want to reset her password, or change other registration info.
-          The lines must be indented, and must begin with '>'.  The initial
-          spaces and '> ' will be stripped off.  $username$ will be replaced by
-          the user's username, $useremail$ by her email address, $hostname$ by
-          the hostname on which the wiki is running (as returned by the
-          hostname system call), $port$ by the port on which the wiki is
-          running, and $resetlink$ by the relative path of a reset link derived
-          from the user's existing hashed password. If your gitit wiki is being
-          proxied to a location other than the root path of $port$, you should
-          change the link to reflect this: for example, to
-          http://$hostname$/path/to/wiki$resetlink$ or
-          http://gitit.$hostname$$resetlink$
-        '';
-      };
-
-      useFeed = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Specifies whether an ATOM feed should be enabled (for the site and
-          for individual pages).
-        '';
-      };
-
-      baseUrl = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          The base URL of the wiki, to be used in constructing feed IDs and RPX
-          token_urls.  Set this if useFeed is false or authentication-method
-          is 'rpx'.
-        '';
-      };
-
-      absoluteUrls = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Make wikilinks absolute with respect to the base-url.  So, for
-          example, in a wiki served at the base URL '/wiki', on a page
-          Sub/Page, the wikilink `[Cactus]()` will produce a link to
-          '/wiki/Cactus' if absoluteUrls is true, and a relative link to
-          'Cactus' (referring to '/wiki/Sub/Cactus') if absolute-urls is 'no'.
-        '';
-      };
-
-      feedDays = mkOption {
-        type = types.int;
-        default = 14;
-        description = lib.mdDoc "Number of days to be included in feeds.";
-      };
-
-      feedRefreshTime = mkOption {
-        type = types.int;
-        default = 60;
-        description = lib.mdDoc "Number of minutes to cache feeds before refreshing.";
-      };
-
-      pdfExport = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          If true, PDF will appear in export options. PDF will be created using
-          pdflatex, which must be installed and in the path. Note that PDF
-          exports create significant additional server load.
-        '';
-      };
-
-      pandocUserData = mkOption {
-        type = with types; nullOr path;
-        default = null;
-        description = lib.mdDoc ''
-          If a directory is specified, this will be searched for pandoc
-          customizations. These can include a templates/ directory for custom
-          templates for various export formats, an S5 directory for custom S5
-          styles, and a reference.odt for ODT exports. If no directory is
-          specified, $HOME/.pandoc will be searched. See pandoc's README for
-          more information.
-        '';
-      };
-
-      xssSanitize = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          If true, all HTML (including that produced by pandoc) is filtered
-          through xss-sanitize.  Set to no only if you trust all of your users.
-        '';
-      };
-
-      oauthClientId = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth client ID";
-      };
-
-      oauthClientSecret = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth client secret";
-      };
-
-      oauthCallback = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth callback URL";
-      };
-
-      oauthAuthorizeEndpoint = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth authorize endpoint";
-      };
-
-      oauthAccessTokenEndpoint = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth access token endpoint";
-      };
-
-      githubOrg = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "Github organization";
-      };
-  };
-
-  configFile = pkgs.writeText "gitit.conf" ''
-    address: ${cfg.address}
-    port: ${toString cfg.port}
-    wiki-title: ${cfg.wikiTitle}
-    repository-type: ${cfg.repositoryType}
-    repository-path: ${cfg.repositoryPath}
-    require-authentication: ${cfg.requireAuthentication}
-    authentication-method: ${cfg.authenticationMethod}
-    user-file: ${cfg.userFile}
-    session-timeout: ${toString cfg.sessionTimeout}
-    static-dir: ${cfg.staticDir}
-    default-page-type: ${cfg.defaultPageType}
-    math: ${cfg.math}
-    mathjax-script: ${cfg.mathJaxScript}
-    show-lhs-bird-tracks: ${toYesNo cfg.showLhsBirdTracks}
-    templates-dir: ${cfg.templatesDir}
-    log-file: ${cfg.logFile}
-    log-level: ${cfg.logLevel}
-    front-page: ${cfg.frontPage}
-    no-delete: ${cfg.noDelete}
-    no-edit: ${cfg.noEdit}
-    default-summary: ${cfg.defaultSummary}
-    table-of-contents: ${toYesNo cfg.tableOfContents}
-    plugins: ${concatStringsSep "," cfg.plugins}
-    use-cache: ${toYesNo cfg.useCache}
-    cache-dir: ${cfg.cacheDir}
-    max-upload-size: ${cfg.maxUploadSize}
-    max-page-size: ${cfg.maxPageSize}
-    debug-mode: ${toYesNo cfg.debugMode}
-    compress-responses: ${toYesNo cfg.compressResponses}
-    mime-types-file: ${cfg.mimeTypesFile}
-    use-recaptcha: ${toYesNo cfg.useReCaptcha}
-    recaptcha-private-key: ${toString cfg.reCaptchaPrivateKey}
-    recaptcha-public-key: ${toString cfg.reCaptchaPublicKey}
-    access-question: ${cfg.accessQuestion}
-    access-question-answers: ${cfg.accessQuestionAnswers}
-    rpx-domain: ${toString cfg.rpxDomain}
-    rpx-key: ${toString cfg.rpxKey}
-    mail-command: ${cfg.mailCommand}
-    reset-password-message: ${cfg.resetPasswordMessage}
-    use-feed: ${toYesNo cfg.useFeed}
-    base-url: ${toString cfg.baseUrl}
-    absolute-urls: ${toYesNo cfg.absoluteUrls}
-    feed-days: ${toString cfg.feedDays}
-    feed-refresh-time: ${toString cfg.feedRefreshTime}
-    pdf-export: ${toYesNo cfg.pdfExport}
-    pandoc-user-data: ${toString cfg.pandocUserData}
-    xss-sanitize: ${toYesNo cfg.xssSanitize}
-
-    [Github]
-    oauthclientid: ${toString cfg.oauthClientId}
-    oauthclientsecret: ${toString cfg.oauthClientSecret}
-    oauthcallback: ${toString cfg.oauthCallback}
-    oauthauthorizeendpoint: ${toString cfg.oauthAuthorizeEndpoint}
-    oauthaccesstokenendpoint: ${toString cfg.oauthAccessTokenEndpoint}
-    github-org: ${toString cfg.githubOrg}
-  '';
-
-in
-
-{
-
-  options.services.gitit = gititOptions;
-
-  config = mkIf cfg.enable {
-
-    users.users.gitit = {
-      group = config.users.groups.gitit.name;
-      description = "Gitit user";
-      home = homeDir;
-      createHome = true;
-      uid = config.ids.uids.gitit;
-    };
-
-    users.groups.gitit.gid = config.ids.gids.gitit;
-
-    systemd.services.gitit = let
-      uid = toString config.ids.uids.gitit;
-      gid = toString config.ids.gids.gitit;
-    in {
-      description = "Git and Pandoc Powered Wiki";
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-      path = with pkgs; [ curl ]
-             ++ optional cfg.pdfExport texlive.combined.scheme-basic
-             ++ optional (cfg.repositoryType == "darcs") darcs
-             ++ optional (cfg.repositoryType == "mercurial") mercurial
-             ++ optional (cfg.repositoryType == "git") git;
-
-      preStart = let
-        gm = "gitit@${config.networking.hostName}";
-      in
-      with cfg; ''
-        chown ${uid}:${gid} -R ${homeDir}
-        for dir in ${repositoryPath} ${staticDir} ${templatesDir} ${cacheDir}
-        do
-          if [ ! -d $dir ]
-          then
-            mkdir -p $dir
-            find $dir -type d -exec chmod 0750 {} +
-            find $dir -type f -exec chmod 0640 {} +
-          fi
-        done
-        cd ${repositoryPath}
-        ${
-          if repositoryType == "darcs" then
-          ''
-          if [ ! -d _darcs ]
-          then
-            darcs initialize
-            echo "${gm}" > _darcs/prefs/email
-          ''
-          else if repositoryType == "mercurial" then
-          ''
-          if [ ! -d .hg ]
-          then
-            hg init
-            cat >> .hg/hgrc <<NAMED
-[ui]
-username = gitit ${gm}
-NAMED
-          ''
-          else
-          ''
-          if [ ! -d  .git ]
-          then
-            git init
-            git config user.email "${gm}"
-            git config user.name "gitit"
-          ''}
-          chown ${uid}:${gid} -R ${repositoryPath}
-          fi
-        cd -
-      '';
-
-      serviceConfig = {
-        User = config.users.users.gitit.name;
-        Group = config.users.groups.gitit.name;
-        ExecStart = with cfg; gititSh haskellPackages extraPackages;
-      };
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/gitlab.md b/nixos/modules/services/misc/gitlab.md
new file mode 100644
index 00000000000..916b23584ed
--- /dev/null
+++ b/nixos/modules/services/misc/gitlab.md
@@ -0,0 +1,112 @@
+# GitLab {#module-services-gitlab}
+
+GitLab is a feature-rich git hosting service.
+
+## Prerequisites {#module-services-gitlab-prerequisites}
+
+The `gitlab` service exposes only an Unix socket at
+`/run/gitlab/gitlab-workhorse.socket`. You need to
+configure a webserver to proxy HTTP requests to the socket.
+
+For instance, the following configuration could be used to use nginx as
+frontend proxy:
+```
+services.nginx = {
+  enable = true;
+  recommendedGzipSettings = true;
+  recommendedOptimisation = true;
+  recommendedProxySettings = true;
+  recommendedTlsSettings = true;
+  virtualHosts."git.example.com" = {
+    enableACME = true;
+    forceSSL = true;
+    locations."/".proxyPass = "http://unix:/run/gitlab/gitlab-workhorse.socket";
+  };
+};
+```
+
+## Configuring {#module-services-gitlab-configuring}
+
+GitLab depends on both PostgreSQL and Redis and will automatically enable
+both services. In the case of PostgreSQL, a database and a role will be
+created.
+
+The default state dir is `/var/gitlab/state`. This is where
+all data like the repositories and uploads will be stored.
+
+A basic configuration with some custom settings could look like this:
+```
+services.gitlab = {
+  enable = true;
+  databasePasswordFile = "/var/keys/gitlab/db_password";
+  initialRootPasswordFile = "/var/keys/gitlab/root_password";
+  https = true;
+  host = "git.example.com";
+  port = 443;
+  user = "git";
+  group = "git";
+  smtp = {
+    enable = true;
+    address = "localhost";
+    port = 25;
+  };
+  secrets = {
+    dbFile = "/var/keys/gitlab/db";
+    secretFile = "/var/keys/gitlab/secret";
+    otpFile = "/var/keys/gitlab/otp";
+    jwsFile = "/var/keys/gitlab/jws";
+  };
+  extraConfig = {
+    gitlab = {
+      email_from = "gitlab-no-reply@example.com";
+      email_display_name = "Example GitLab";
+      email_reply_to = "gitlab-no-reply@example.com";
+      default_projects_features = { builds = false; };
+    };
+  };
+};
+```
+
+If you're setting up a new GitLab instance, generate new
+secrets. You for instance use
+`tr -dc A-Za-z0-9 < /dev/urandom | head -c 128 > /var/keys/gitlab/db` to
+generate a new db secret. Make sure the files can be read by, and
+only by, the user specified by
+[services.gitlab.user](#opt-services.gitlab.user). GitLab
+encrypts sensitive data stored in the database. If you're restoring
+an existing GitLab instance, you must specify the secrets secret
+from `config/secrets.yml` located in your GitLab
+state folder.
+
+When `incoming_mail.enabled` is set to `true`
+in [extraConfig](#opt-services.gitlab.extraConfig) an additional
+service called `gitlab-mailroom` is enabled for fetching incoming mail.
+
+Refer to [](#ch-options) for all available configuration
+options for the [services.gitlab](#opt-services.gitlab.enable) module.
+
+## Maintenance {#module-services-gitlab-maintenance}
+
+### Backups {#module-services-gitlab-maintenance-backups}
+
+Backups can be configured with the options in
+[services.gitlab.backup](#opt-services.gitlab.backup.keepTime). Use
+the [services.gitlab.backup.startAt](#opt-services.gitlab.backup.startAt)
+option to configure regular backups.
+
+To run a manual backup, start the `gitlab-backup` service:
+```ShellSession
+$ systemctl start gitlab-backup.service
+```
+
+### Rake tasks {#module-services-gitlab-maintenance-rake}
+
+You can run GitLab's rake tasks with `gitlab-rake`
+which will be available on the system when GitLab is enabled. You
+will have to run the command as the user that you configured to run
+GitLab with.
+
+A list of all available rake tasks can be obtained by running:
+```ShellSession
+$ sudo -u git -H gitlab-rake -T
+```
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index e7c707228f1..a497fbb300d 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -9,12 +9,10 @@ let
   toml = pkgs.formats.toml {};
   yaml = pkgs.formats.yaml {};
 
-  ruby = cfg.packages.gitlab.ruby;
-
   postgresqlPackage = if config.services.postgresql.enable then
                         config.services.postgresql.package
                       else
-                        pkgs.postgresql_12;
+                        pkgs.postgresql_13;
 
   gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket";
   gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket";
@@ -40,15 +38,13 @@ let
 
   gitalyToml = pkgs.writeText "gitaly.toml" ''
     socket_path = "${lib.escape ["\""] gitalySocket}"
+    runtime_dir = "/run/gitaly"
     bin_dir = "${cfg.packages.gitaly}/bin"
     prometheus_listen_addr = "localhost:9236"
 
     [git]
     bin_path = "${pkgs.git}/bin/git"
 
-    [gitaly-ruby]
-    dir = "${cfg.packages.gitaly.ruby}"
-
     [gitlab-shell]
     dir = "${cfg.packages.gitlab-shell}"
 
@@ -88,10 +84,8 @@ let
     };
   };
 
-  pagesArgs = [
-    "-pages-domain" gitlabConfig.production.pages.host
-    "-pages-root" "${gitlabConfig.production.shared.path}/pages"
-  ] ++ cfg.pagesExtraArgs;
+  # Redis configuration file
+  resqueYml = pkgs.writeText "resque.yml" (builtins.toJSON redisConfig);
 
   gitlabConfig = {
     # These are the default settings from config/gitlab.example.yml
@@ -158,8 +152,15 @@ let
         api_url = "http://${config.services.dockerRegistry.listenAddress}:${toString config.services.dockerRegistry.port}/";
         issuer = cfg.registry.issuer;
       };
+      elasticsearch.indexer_path = "${pkgs.gitlab-elasticsearch-indexer}/bin/gitlab-elasticsearch-indexer";
       extra = {};
       uploads.storage_path = cfg.statePath;
+      pages = optionalAttrs cfg.pages.enable {
+        enabled = cfg.pages.enable;
+        port = 8090;
+        host = cfg.pages.settings.pages-domain;
+        secret_file = cfg.pages.settings.api-secret-key;
+      };
     };
   };
 
@@ -170,7 +171,6 @@ let
     SCHEMA = "${cfg.statePath}/db/structure.sql";
     GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads";
     GITLAB_LOG_PATH = "${cfg.statePath}/log";
-    GITLAB_REDIS_CONFIG_FILE = pkgs.writeText "redis.yml" (builtins.toJSON redisConfig);
     prometheus_multiproc_dir = "/run/gitlab";
     RAILS_ENV = "production";
     MALLOC_ARENA_MAX = "2";
@@ -245,6 +245,7 @@ in {
     (mkRenamedOptionModule [ "services" "gitlab" "backupPath" ] [ "services" "gitlab" "backup" "path" ])
     (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "")
     (mkRemovedOptionModule [ "services" "gitlab" "logrotate" "extraConfig" ] "Modify services.logrotate.settings.gitlab directly instead")
+    (mkRemovedOptionModule [ "services" "gitlab" "pagesExtraArgs" ] "Use services.gitlab.pages.settings instead")
   ];
 
   options = {
@@ -553,6 +554,20 @@ in {
           default = false;
           description = lib.mdDoc "Enable GitLab container registry.";
         };
+        package = mkOption {
+          type = types.package;
+          default =
+            if versionAtLeast config.system.stateVersion "23.11"
+            then pkgs.gitlab-container-registry
+            else pkgs.docker-distribution;
+          defaultText = literalExpression "pkgs.docker-distribution";
+          description = lib.mdDoc ''
+            Container registry package to use.
+
+            External container registries such as `pkgs.docker-distribution` are not supported
+            anymore since GitLab 16.0.0.
+          '';
+        };
         host = mkOption {
           type = types.str;
           default = config.services.gitlab.host;
@@ -666,10 +681,127 @@ in {
         };
       };
 
-      pagesExtraArgs = mkOption {
-        type = types.listOf types.str;
-        default = [ "-listen-proxy" "127.0.0.1:8090" ];
-        description = lib.mdDoc "Arguments to pass to the gitlab-pages daemon";
+      pages.enable = mkEnableOption (lib.mdDoc "the GitLab Pages service");
+
+      pages.settings = mkOption {
+        example = literalExpression ''
+          {
+            pages-domain = "example.com";
+            auth-client-id = "generated-id-xxxxxxx";
+            auth-client-secret = { _secret = "/var/keys/auth-client-secret"; };
+            auth-redirect-uri = "https://projects.example.com/auth";
+            auth-secret = { _secret = "/var/keys/auth-secret"; };
+            auth-server = "https://gitlab.example.com";
+          }
+        '';
+
+        description = lib.mdDoc ''
+          Configuration options to set in the GitLab Pages config
+          file.
+
+          Options containing secret data should be set to an attribute
+          set containing the attribute `_secret` - a string pointing
+          to a file containing the value the option should be set
+          to. See the example to get a better picture of this: in the
+          resulting configuration file, the `auth-client-secret` and
+          `auth-secret` keys will be set to the contents of the
+          {file}`/var/keys/auth-client-secret` and
+          {file}`/var/keys/auth-secret` files respectively.
+        '';
+
+        type = types.submodule {
+          freeformType = with types; attrsOf (nullOr (oneOf [ str int bool attrs ]));
+
+          options = {
+            listen-http = mkOption {
+              type = with types; listOf str;
+              apply = x: if x == [] then null else lib.concatStringsSep "," x;
+              default = [];
+              description = lib.mdDoc ''
+                The address(es) to listen on for HTTP requests.
+              '';
+            };
+
+            listen-https = mkOption {
+              type = with types; listOf str;
+              apply = x: if x == [] then null else lib.concatStringsSep "," x;
+              default = [];
+              description = lib.mdDoc ''
+                The address(es) to listen on for HTTPS requests.
+              '';
+            };
+
+            listen-proxy = mkOption {
+              type = with types; listOf str;
+              apply = x: if x == [] then null else lib.concatStringsSep "," x;
+              default = [ "127.0.0.1:8090" ];
+              description = lib.mdDoc ''
+                The address(es) to listen on for proxy requests.
+              '';
+            };
+
+            artifacts-server = mkOption {
+              type = with types; nullOr str;
+              default = "http${optionalString cfg.https "s"}://${cfg.host}/api/v4";
+              defaultText = "http(s)://<services.gitlab.host>/api/v4";
+              example = "https://gitlab.example.com/api/v4";
+              description = lib.mdDoc ''
+                API URL to proxy artifact requests to.
+              '';
+            };
+
+            gitlab-server = mkOption {
+              type = with types; nullOr str;
+              default = "http${optionalString cfg.https "s"}://${cfg.host}";
+              defaultText = "http(s)://<services.gitlab.host>";
+              example = "https://gitlab.example.com";
+              description = lib.mdDoc ''
+                Public GitLab server URL.
+              '';
+            };
+
+            internal-gitlab-server = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              defaultText = "http(s)://<services.gitlab.host>";
+              example = "https://gitlab.example.internal";
+              description = lib.mdDoc ''
+                Internal GitLab server used for API requests, useful
+                if you want to send that traffic over an internal load
+                balancer. By default, the value of
+                `services.gitlab.pages.settings.gitlab-server` is
+                used.
+              '';
+            };
+
+            api-secret-key = mkOption {
+              type = with types; nullOr str;
+              default = "${cfg.statePath}/gitlab_pages_secret";
+              internal = true;
+              description = lib.mdDoc ''
+                File with secret key used to authenticate with the
+                GitLab API.
+              '';
+            };
+
+            pages-domain = mkOption {
+              type = with types; nullOr str;
+              example = "example.com";
+              description = lib.mdDoc ''
+                The domain to serve static pages on.
+              '';
+            };
+
+            pages-root = mkOption {
+              type = types.str;
+              default = "${gitlabConfig.production.shared.path}/pages";
+              defaultText = literalExpression ''config.${opt.extraConfig}.production.shared.path + "/pages"'';
+              description = lib.mdDoc ''
+                The directory where pages are stored.
+              '';
+            };
+          };
+        };
       };
 
       secrets.secretFile = mkOption {
@@ -950,6 +1082,13 @@ in {
   };
 
   config = mkIf cfg.enable {
+    warnings = [
+      (mkIf
+        (cfg.registry.enable && versionAtLeast (getVersion cfg.packages.gitlab) "16.0.0" && cfg.registry.package == pkgs.docker-distribution)
+        ''Support for container registries other than gitlab-container-registry has ended since GitLab 16.0.0 and is scheduled for removal in a future release.
+          Please back up your data and migrate to the gitlab-container-registry package.''
+      )
+    ];
 
     assertions = [
       {
@@ -981,8 +1120,8 @@ in {
         message = "services.gitlab.secrets.jwsFile must be set!";
       }
       {
-        assertion = versionAtLeast postgresqlPackage.version "12.0.0";
-        message = "PostgreSQL >=12 is required to run GitLab 14. Follow the instructions in the manual section for upgrading PostgreSQL here: https://nixos.org/manual/nixos/stable/index.html#module-services-postgres-upgrading";
+        assertion = versionAtLeast postgresqlPackage.version "13.6.0";
+        message = "PostgreSQL >=13.6 is required to run GitLab 16. Follow the instructions in the manual section for upgrading PostgreSQL here: https://nixos.org/manual/nixos/stable/index.html#module-services-postgres-upgrading";
       }
     ];
 
@@ -1093,9 +1232,10 @@ in {
     services.dockerRegistry = optionalAttrs cfg.registry.enable {
       enable = true;
       enableDelete = true; # This must be true, otherwise GitLab won't manage it correctly
+      package = cfg.registry.package;
       extraConfig = {
         auth.token = {
-          realm = "http${if cfg.https == true then "s" else ""}://${cfg.host}/jwt/auth";
+          realm = "http${optionalString (cfg.https == true) "s"}://${cfg.host}/jwt/auth";
           service = cfg.registry.serviceName;
           issuer = cfg.registry.issuer;
           rootcertbundle = cfg.registry.certFile;
@@ -1142,6 +1282,7 @@ in {
       "d ${gitlabConfig.production.shared.path}/pages 0750 ${cfg.user} ${cfg.group} -"
       "d ${gitlabConfig.production.shared.path}/registry 0750 ${cfg.user} ${cfg.group} -"
       "d ${gitlabConfig.production.shared.path}/terraform_state 0750 ${cfg.user} ${cfg.group} -"
+      "d ${gitlabConfig.production.shared.path}/ci_secure_files 0750 ${cfg.user} ${cfg.group} -"
       "L+ /run/gitlab/config - - - - ${cfg.statePath}/config"
       "L+ /run/gitlab/log - - - - ${cfg.statePath}/log"
       "L+ /run/gitlab/tmp - - - - ${cfg.statePath}/tmp"
@@ -1195,6 +1336,7 @@ in {
           cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db
           ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb
           ln -sf ${cableYml} ${cfg.statePath}/config/cable.yml
+          ln -sf ${resqueYml} ${cfg.statePath}/config/resque.yml
 
           ${cfg.packages.gitlab-shell}/bin/install
 
@@ -1209,6 +1351,9 @@ in {
             umask u=rwx,g=,o=
 
             openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
+            ${optionalString cfg.pages.enable ''
+                openssl rand -base64 32 > ${cfg.pages.settings.api-secret-key}
+            ''}
 
             rm -f '${cfg.statePath}/config/database.yml'
 
@@ -1339,10 +1484,7 @@ in {
       partOf = [ "gitlab.target" ];
       path = with pkgs; [
         openssh
-        procps  # See https://gitlab.com/gitlab-org/gitaly/issues/1562
         git
-        cfg.packages.gitaly.rubyEnv
-        cfg.packages.gitaly.rubyEnv.wrappedRuby
         gzip
         bzip2
       ];
@@ -1353,32 +1495,71 @@ in {
         TimeoutSec = "infinity";
         Restart = "on-failure";
         WorkingDirectory = gitlabEnv.HOME;
+        RuntimeDirectory = "gitaly";
         ExecStart = "${cfg.packages.gitaly}/bin/gitaly ${gitalyToml}";
       };
     };
 
-    systemd.services.gitlab-pages = mkIf (gitlabConfig.production.pages.enabled or false) {
-      description = "GitLab static pages daemon";
-      after = [ "network.target" "gitlab-config.service" ];
-      bindsTo = [ "gitlab-config.service" ];
-      wantedBy = [ "gitlab.target" ];
-      partOf = [ "gitlab.target" ];
-
-      path = [ pkgs.unzip ];
-
-      serviceConfig = {
-        Type = "simple";
-        TimeoutSec = "infinity";
-        Restart = "on-failure";
-
-        User = cfg.user;
-        Group = cfg.group;
-
-        ExecStart = "${cfg.packages.pages}/bin/gitlab-pages ${escapeShellArgs pagesArgs}";
-        WorkingDirectory = gitlabEnv.HOME;
-      };
+    services.gitlab.pages.settings = {
+      api-secret-key = "${cfg.statePath}/gitlab_pages_secret";
     };
 
+    systemd.services.gitlab-pages =
+      let
+        filteredConfig = filterAttrs (_: v: v != null) cfg.pages.settings;
+        isSecret = v: isAttrs v && v ? _secret && isString v._secret;
+        mkPagesKeyValue = lib.generators.toKeyValue {
+          mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" rec {
+            mkValueString = v:
+              if isInt           v then toString v
+              else if isString   v then v
+              else if true  ==   v then "true"
+              else if false ==   v then "false"
+              else if isSecret   v then builtins.hashString "sha256" v._secret
+              else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}";
+          };
+        };
+        secretPaths = lib.catAttrs "_secret" (lib.collect isSecret filteredConfig);
+        mkSecretReplacement = file: ''
+          replace-secret ${lib.escapeShellArgs [ (builtins.hashString "sha256" file) file "/run/gitlab-pages/gitlab-pages.conf" ]}
+        '';
+        secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
+        configFile = pkgs.writeText "gitlab-pages.conf" (mkPagesKeyValue filteredConfig);
+      in
+        mkIf cfg.pages.enable {
+          description = "GitLab static pages daemon";
+          after = [ "network.target" "gitlab-config.service" "gitlab.service" ];
+          bindsTo = [ "gitlab-config.service" "gitlab.service" ];
+          wantedBy = [ "gitlab.target" ];
+          partOf = [ "gitlab.target" ];
+
+          path = with pkgs; [
+            unzip
+            replace-secret
+          ];
+
+          serviceConfig = {
+            Type = "simple";
+            TimeoutSec = "infinity";
+            Restart = "on-failure";
+
+            User = cfg.user;
+            Group = cfg.group;
+
+            ExecStartPre = pkgs.writeShellScript "gitlab-pages-pre-start" ''
+              set -o errexit -o pipefail -o nounset
+              shopt -s dotglob nullglob inherit_errexit
+
+              install -m u=rw ${configFile} /run/gitlab-pages/gitlab-pages.conf
+              ${secretReplacements}
+            '';
+            ExecStart = "${cfg.packages.pages}/bin/gitlab-pages -config=/run/gitlab-pages/gitlab-pages.conf";
+            WorkingDirectory = gitlabEnv.HOME;
+            RuntimeDirectory = "gitlab-pages";
+            RuntimeDirectoryMode = "0700";
+          };
+        };
+
     systemd.services.gitlab-workhorse = {
       after = [ "network.target" ];
       wantedBy = [ "gitlab.target" ];
@@ -1464,6 +1645,7 @@ in {
         nodejs
         procps
         gnupg
+        gzip
       ];
       serviceConfig = {
         Type = "notify";
@@ -1502,6 +1684,6 @@ in {
 
   };
 
-  meta.doc = ./gitlab.xml;
-
+  meta.doc = ./gitlab.md;
+  meta.maintainers = teams.gitlab.members;
 }
diff --git a/nixos/modules/services/misc/gitlab.xml b/nixos/modules/services/misc/gitlab.xml
deleted file mode 100644
index 9816fdac7dd..00000000000
--- a/nixos/modules/services/misc/gitlab.xml
+++ /dev/null
@@ -1,151 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-gitlab">
- <title>GitLab</title>
- <para>
-  GitLab is a feature-rich git hosting service.
- </para>
- <section xml:id="module-services-gitlab-prerequisites">
-  <title>Prerequisites</title>
-
-  <para>
-   The <literal>gitlab</literal> service exposes only an Unix socket at
-   <literal>/run/gitlab/gitlab-workhorse.socket</literal>. You need to
-   configure a webserver to proxy HTTP requests to the socket.
-  </para>
-
-  <para>
-   For instance, the following configuration could be used to use nginx as
-   frontend proxy:
-<programlisting>
-<link linkend="opt-services.nginx.enable">services.nginx</link> = {
-  <link linkend="opt-services.nginx.enable">enable</link> = true;
-  <link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true;
-  <link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true;
-  <link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true;
-  <link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true;
-  <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link>."git.example.com" = {
-    <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
-    <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
-    <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.proxyPass">locations."/".proxyPass</link> = "http://unix:/run/gitlab/gitlab-workhorse.socket";
-  };
-};
-</programlisting>
-  </para>
- </section>
- <section xml:id="module-services-gitlab-configuring">
-  <title>Configuring</title>
-
-  <para>
-   GitLab depends on both PostgreSQL and Redis and will automatically enable
-   both services. In the case of PostgreSQL, a database and a role will be
-   created.
-  </para>
-
-  <para>
-   The default state dir is <literal>/var/gitlab/state</literal>. This is where
-   all data like the repositories and uploads will be stored.
-  </para>
-
-  <para>
-   A basic configuration with some custom settings could look like this:
-<programlisting>
-services.gitlab = {
-  <link linkend="opt-services.gitlab.enable">enable</link> = true;
-  <link linkend="opt-services.gitlab.databasePasswordFile">databasePasswordFile</link> = "/var/keys/gitlab/db_password";
-  <link linkend="opt-services.gitlab.initialRootPasswordFile">initialRootPasswordFile</link> = "/var/keys/gitlab/root_password";
-  <link linkend="opt-services.gitlab.https">https</link> = true;
-  <link linkend="opt-services.gitlab.host">host</link> = "git.example.com";
-  <link linkend="opt-services.gitlab.port">port</link> = 443;
-  <link linkend="opt-services.gitlab.user">user</link> = "git";
-  <link linkend="opt-services.gitlab.group">group</link> = "git";
-  smtp = {
-    <link linkend="opt-services.gitlab.smtp.enable">enable</link> = true;
-    <link linkend="opt-services.gitlab.smtp.address">address</link> = "localhost";
-    <link linkend="opt-services.gitlab.smtp.port">port</link> = 25;
-  };
-  secrets = {
-    <link linkend="opt-services.gitlab.secrets.dbFile">dbFile</link> = "/var/keys/gitlab/db";
-    <link linkend="opt-services.gitlab.secrets.secretFile">secretFile</link> = "/var/keys/gitlab/secret";
-    <link linkend="opt-services.gitlab.secrets.otpFile">otpFile</link> = "/var/keys/gitlab/otp";
-    <link linkend="opt-services.gitlab.secrets.jwsFile">jwsFile</link> = "/var/keys/gitlab/jws";
-  };
-  <link linkend="opt-services.gitlab.extraConfig">extraConfig</link> = {
-    gitlab = {
-      email_from = "gitlab-no-reply@example.com";
-      email_display_name = "Example GitLab";
-      email_reply_to = "gitlab-no-reply@example.com";
-      default_projects_features = { builds = false; };
-    };
-  };
-};
-</programlisting>
-  </para>
-
-  <para>
-   If you're setting up a new GitLab instance, generate new
-   secrets. You for instance use <literal>tr -dc A-Za-z0-9 &lt;
-   /dev/urandom | head -c 128 &gt; /var/keys/gitlab/db</literal> to
-   generate a new db secret. Make sure the files can be read by, and
-   only by, the user specified by <link
-   linkend="opt-services.gitlab.user">services.gitlab.user</link>. GitLab
-   encrypts sensitive data stored in the database. If you're restoring
-   an existing GitLab instance, you must specify the secrets secret
-   from <literal>config/secrets.yml</literal> located in your GitLab
-   state folder.
-  </para>
-
-  <para>
-    When <literal>incoming_mail.enabled</literal> is set to <literal>true</literal>
-    in <link linkend="opt-services.gitlab.extraConfig">extraConfig</link> an additional
-    service called <literal>gitlab-mailroom</literal> is enabled for fetching incoming mail.
-  </para>
-
-  <para>
-   Refer to <xref linkend="ch-options" /> for all available configuration
-   options for the
-   <link linkend="opt-services.gitlab.enable">services.gitlab</link> module.
-  </para>
- </section>
- <section xml:id="module-services-gitlab-maintenance">
-  <title>Maintenance</title>
-
-  <section xml:id="module-services-gitlab-maintenance-backups">
-   <title>Backups</title>
-   <para>
-     Backups can be configured with the options in <link
-     linkend="opt-services.gitlab.backup.keepTime">services.gitlab.backup</link>. Use
-     the <link
-     linkend="opt-services.gitlab.backup.startAt">services.gitlab.backup.startAt</link>
-     option to configure regular backups.
-   </para>
-
-   <para>
-     To run a manual backup, start the <literal>gitlab-backup</literal> service:
-<screen>
-<prompt>$ </prompt>systemctl start gitlab-backup.service
-</screen>
-   </para>
-  </section>
-
-  <section xml:id="module-services-gitlab-maintenance-rake">
-   <title>Rake tasks</title>
-
-   <para>
-    You can run GitLab's rake tasks with <literal>gitlab-rake</literal>
-    which will be available on the system when GitLab is enabled. You
-    will have to run the command as the user that you configured to run
-    GitLab with.
-   </para>
-
-   <para>
-    A list of all available rake tasks can be obtained by running:
-<screen>
-<prompt>$ </prompt>sudo -u git -H gitlab-rake -T
-</screen>
-   </para>
-  </section>
- </section>
-</chapter>
diff --git a/nixos/modules/services/misc/gollum.nix b/nixos/modules/services/misc/gollum.nix
index 4eec9610b5e..d607e92e5ec 100644
--- a/nixos/modules/services/misc/gollum.nix
+++ b/nixos/modules/services/misc/gollum.nix
@@ -91,18 +91,30 @@ in
         The package used in the service
       '';
     };
+
+    user = mkOption {
+      type = types.str;
+      default = "gollum";
+      description = lib.mdDoc "Specifies the owner of the wiki directory";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "gollum";
+      description = lib.mdDoc "Specifies the owner group of the wiki directory";
+    };
   };
 
   config = mkIf cfg.enable {
 
-    users.users.gollum = {
-      group = config.users.users.gollum.name;
+    users.users.gollum = mkIf (cfg.user == "gollum") {
+      group = cfg.group;
       description = "Gollum user";
       createHome = false;
       isSystemUser = true;
     };
 
-    users.groups.gollum = { };
+    users.groups."${cfg.group}" = { };
 
     systemd.tmpfiles.rules = [
       "d '${cfg.stateDir}' - ${config.users.users.gollum.name} ${config.users.groups.gollum.name} - -"
@@ -120,8 +132,8 @@ in
       '';
 
       serviceConfig = {
-        User = config.users.users.gollum.name;
-        Group = config.users.groups.gollum.name;
+        User = cfg.user;
+        Group = cfg.group;
         WorkingDirectory = cfg.stateDir;
         ExecStart = ''
           ${cfg.package}/bin/gollum \
@@ -142,5 +154,5 @@ in
     };
   };
 
-  meta.maintainers = with lib.maintainers; [ erictapen bbenno ];
+  meta.maintainers = with lib.maintainers; [ erictapen bbenno joscha ];
 }
diff --git a/nixos/modules/services/misc/gpsd.nix b/nixos/modules/services/misc/gpsd.nix
index ec0a8e1eaa1..ce0f9bb3ba2 100644
--- a/nixos/modules/services/misc/gpsd.nix
+++ b/nixos/modules/services/misc/gpsd.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
 with lib;
 
@@ -8,12 +8,15 @@ let
   gid = config.ids.gids.gpsd;
   cfg = config.services.gpsd;
 
-in
-
-{
+in {
 
   ###### interface
 
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "gpsd" "device" ]
+      "Use `services.gpsd.devices` instead.")
+  ];
+
   options = {
 
     services.gpsd = {
@@ -22,17 +25,21 @@ in
         type = types.bool;
         default = false;
         description = lib.mdDoc ''
-          Whether to enable `gpsd', a GPS service daemon.
+          Whether to enable `gpsd`, a GPS service daemon.
         '';
       };
 
-      device = mkOption {
-        type = types.str;
-        default = "/dev/ttyUSB0";
+      devices = mkOption {
+        type = types.listOf types.str;
+        default = [ "/dev/ttyUSB0" ];
         description = lib.mdDoc ''
-          A device may be a local serial device for GPS input, or a URL of the form:
-               `[{dgpsip|ntrip}://][user:passwd@]host[:port][/stream]`
-          in which case it specifies an input source for DGPS or ntrip data.
+          List of devices that `gpsd` should subscribe to.
+
+          A device may be a local serial device for GPS input, or a
+          URL of the form:
+          `[{dgpsip|ntrip}://][user:passwd@]host[:port][/stream]` in
+          which case it specifies an input source for DGPS or ntrip
+          data.
         '';
       };
 
@@ -89,17 +96,16 @@ in
 
   };
 
-
   ###### implementation
 
   config = mkIf cfg.enable {
 
-    users.users.gpsd =
-      { inherit uid;
-        group = "gpsd";
-        description = "gpsd daemon user";
-        home = "/var/empty";
-      };
+    users.users.gpsd = {
+      inherit uid;
+      group = "gpsd";
+      description = "gpsd daemon user";
+      home = "/var/empty";
+    };
 
     users.groups.gpsd = { inherit gid; };
 
@@ -109,13 +115,15 @@ in
       after = [ "network.target" ];
       serviceConfig = {
         Type = "forking";
-        ExecStart = ''
+        ExecStart = let
+          devices = utils.escapeSystemdExecArgs cfg.devices;
+        in ''
           ${pkgs.gpsd}/sbin/gpsd -D "${toString cfg.debugLevel}"  \
             -S "${toString cfg.port}"                             \
             ${optionalString cfg.readonly "-b"}                   \
             ${optionalString cfg.nowait "-n"}                     \
             ${optionalString cfg.listenany "-G"}                  \
-            "${cfg.device}"
+            ${devices}
         '';
       };
     };
diff --git a/nixos/modules/services/misc/heisenbridge.nix b/nixos/modules/services/misc/heisenbridge.nix
index d07e0e42046..822a09d7cd4 100644
--- a/nixos/modules/services/misc/heisenbridge.nix
+++ b/nixos/modules/services/misc/heisenbridge.nix
@@ -137,7 +137,7 @@ in
         mv -f ${registrationFile}.new ${registrationFile}
 
         # Grant Synapse access to the registration
-        if ${getBin pkgs.glibc}/bin/getent group matrix-synapse > /dev/null; then
+        if ${pkgs.getent}/bin/getent group matrix-synapse > /dev/null; then
           chgrp -v matrix-synapse ${registrationFile}
           chmod -v g+r ${registrationFile}
         fi
diff --git a/nixos/modules/services/misc/input-remapper.nix b/nixos/modules/services/misc/input-remapper.nix
index 51e1abdc98a..3f6d97f8573 100644
--- a/nixos/modules/services/misc/input-remapper.nix
+++ b/nixos/modules/services/misc/input-remapper.nix
@@ -6,8 +6,8 @@ let cfg = config.services.input-remapper; in
 {
   options = {
     services.input-remapper = {
-      enable = mkEnableOption (lib.mdDoc "input-remapper, an easy to use tool to change the mapping of your input device buttons.");
-      package = options.mkPackageOption pkgs "input-remapper" { };
+      enable = mkEnableOption (lib.mdDoc "input-remapper, an easy to use tool to change the mapping of your input device buttons");
+      package = mkPackageOptionMD pkgs "input-remapper" { };
       enableUdevRules = mkEnableOption (lib.mdDoc "udev rules added by input-remapper to handle hotplugged devices. Currently disabled by default due to https://github.com/sezanzeb/input-remapper/issues/140");
       serviceWantedBy = mkOption {
         default = [ "graphical.target" ];
diff --git a/nixos/modules/services/misc/jellyseerr.nix b/nixos/modules/services/misc/jellyseerr.nix
new file mode 100644
index 00000000000..31e0c5beb67
--- /dev/null
+++ b/nixos/modules/services/misc/jellyseerr.nix
@@ -0,0 +1,62 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.jellyseerr;
+in
+{
+  meta.maintainers = [ maintainers.camillemndn ];
+
+  options.services.jellyseerr = {
+    enable = mkEnableOption (mdDoc ''Jellyseerr, a requests manager for Jellyfin'');
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = mdDoc ''Open port in the firewall for the Jellyseerr web interface.'';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5055;
+      description = mdDoc ''The port which the Jellyseerr web UI should listen to.'';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.jellyseerr = {
+      description = "Jellyseerr, a requests manager for Jellyfin";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment.PORT = toString cfg.port;
+      serviceConfig = {
+        Type = "exec";
+        StateDirectory = "jellyseerr";
+        WorkingDirectory = "${pkgs.jellyseerr}/libexec/jellyseerr/deps/jellyseerr";
+        DynamicUser = true;
+        ExecStart = "${pkgs.jellyseerr}/bin/jellyseerr";
+        BindPaths = [ "/var/lib/jellyseerr/:${pkgs.jellyseerr}/libexec/jellyseerr/deps/jellyseerr/config/" ];
+        Restart = "on-failure";
+        ProtectHome = true;
+        ProtectSystem = "strict";
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        NoNewPrivileges = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+        PrivateMounts = true;
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/klipper.nix b/nixos/modules/services/misc/klipper.nix
index a2158e9461b..ad881d4462a 100644
--- a/nixos/modules/services/misc/klipper.nix
+++ b/nixos/modules/services/misc/klipper.nix
@@ -23,6 +23,16 @@ in
         description = lib.mdDoc "The Klipper package.";
       };
 
+      logFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/lib/klipper/klipper.log";
+        description = lib.mdDoc ''
+          Path of the file Klipper should log to.
+          If `null`, it logs to stdout, which is not recommended by upstream.
+        '';
+      };
+
       inputTTY = mkOption {
         type = types.path;
         default = "/run/klipper/tty";
@@ -135,7 +145,7 @@ in
       }
       {
         assertion = (cfg.configFile != null) != (cfg.settings != null);
-        message = "You need to either specify services.klipper.settings or services.klipper.defaultConfig.";
+        message = "You need to either specify services.klipper.settings or services.klipper.configFile.";
       }
     ];
 
@@ -151,7 +161,9 @@ in
     systemd.services.klipper =
       let
         klippyArgs = "--input-tty=${cfg.inputTTY}"
-          + optionalString (cfg.apiSocket != null) " --api-server=${cfg.apiSocket}";
+          + optionalString (cfg.apiSocket != null) " --api-server=${cfg.apiSocket}"
+          + optionalString (cfg.logFile != null) " --logfile=${cfg.logFile}"
+        ;
         printerConfigPath =
           if cfg.mutableConfig
           then cfg.mutableConfigFolder + "/printer.cfg"
@@ -177,7 +189,7 @@ in
         '';
 
         serviceConfig = {
-          ExecStart = "${cfg.package}/lib/klipper/klippy.py ${klippyArgs} ${printerConfigPath}";
+          ExecStart = "${cfg.package}/bin/klippy ${klippyArgs} ${printerConfigPath}";
           RuntimeDirectory = "klipper";
           StateDirectory = "klipper";
           SupplementaryGroups = [ "dialout" ];
diff --git a/nixos/modules/services/misc/mbpfan.nix b/nixos/modules/services/misc/mbpfan.nix
index d467aa87976..e75c3525414 100644
--- a/nixos/modules/services/misc/mbpfan.nix
+++ b/nixos/modules/services/misc/mbpfan.nix
@@ -1,10 +1,9 @@
 { config, lib, pkgs, ... }:
-
 with lib;
 
 let
   cfg = config.services.mbpfan;
-  verbose = if cfg.verbose then "v" else "";
+  verbose = optionalString cfg.verbose "v";
   settingsFormat = pkgs.formats.ini {};
   settingsFile = settingsFormat.generate "mbpfan.ini" cfg.settings;
 
@@ -16,17 +15,19 @@ in {
       type = types.package;
       default = pkgs.mbpfan;
       defaultText = literalExpression "pkgs.mbpfan";
-      description = lib.mdDoc ''
-        The package used for the mbpfan daemon.
-      '';
+      description = lib.mdDoc "The package used for the mbpfan daemon.";
     };
 
     verbose = mkOption {
       type = types.bool;
       default = false;
-      description = lib.mdDoc ''
-        If true, sets the log level to verbose.
-      '';
+      description = lib.mdDoc "If true, sets the log level to verbose.";
+    };
+
+    aggressive = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc "If true, favors higher default fan speeds.";
     };
 
     settings = mkOption {
@@ -35,24 +36,14 @@ in {
       type = types.submodule {
         freeformType = settingsFormat.type;
 
-        options.general.min_fan1_speed = mkOption {
-          type = types.nullOr types.int;
-          default = 2000;
-          description = lib.mdDoc ''
-            You can check minimum and maximum fan limits with
-            `cat /sys/devices/platform/applesmc.768/fan*_min` and
-            `cat /sys/devices/platform/applesmc.768/fan*_max` respectively.
-            Setting to null implies using default value from applesmc.
-          '';
-        };
         options.general.low_temp = mkOption {
           type = types.int;
-          default = 55;
+          default = 63;
           description = lib.mdDoc "If temperature is below this, fans will run at minimum speed.";
         };
         options.general.high_temp = mkOption {
           type = types.int;
-          default = 58;
+          default = 66;
           description = lib.mdDoc "If temperature is above this, fan speed will gradually increase.";
         };
         options.general.max_temp = mkOption {
@@ -79,10 +70,16 @@ in {
   ];
 
   config = mkIf cfg.enable {
-    boot.kernelModules = [ "coretemp" "applesmc" ];
+    services.mbpfan.settings = mkIf cfg.aggressive {
+      general.min_fan1_speed = mkDefault 2000;
+      general.low_temp = mkDefault 55;
+      general.high_temp = mkDefault 58;
+      general.max_temp = mkDefault 70;
+    };
 
-    environment.etc."mbpfan.conf".source = settingsFile;
+    boot.kernelModules = [ "coretemp" "applesmc" ];
     environment.systemPackages = [ cfg.package ];
+    environment.etc."mbpfan.conf".source = settingsFile;
 
     systemd.services.mbpfan = {
       description = "A fan manager daemon for MacBook Pro";
diff --git a/nixos/modules/services/misc/moonraker.nix b/nixos/modules/services/misc/moonraker.nix
index 62064b5d90f..7e306d718e0 100644
--- a/nixos/modules/services/misc/moonraker.nix
+++ b/nixos/modules/services/misc/moonraker.nix
@@ -11,6 +11,8 @@ let
       else lib.concatMapStrings (s: "\n  ${generators.mkValueStringDefault {} s}") l;
     mkKeyValue = generators.mkKeyValueDefault {} ":";
   };
+
+  unifiedConfigDir = cfg.stateDir + "/config";
 in {
   options = {
     services.moonraker = {
@@ -30,11 +32,10 @@ in {
       };
 
       configDir = mkOption {
-        type = types.path;
-        default = cfg.stateDir + "/config";
-        defaultText = literalExpression ''config.${opt.stateDir} + "/config"'';
+        type = types.nullOr types.path;
+        default = null;
         description = lib.mdDoc ''
-          The directory containing client-writable configuration files.
+          Deprecated directory containing client-writable configuration files.
 
           Clients will be able to edit files in this directory via the API. This directory must be writable.
         '';
@@ -71,7 +72,7 @@ in {
         example = {
           authorization = {
             trusted_clients = [ "10.0.0.0/24" ];
-            cors_domains = [ "https://app.fluidd.xyz" ];
+            cors_domains = [ "https://app.fluidd.xyz" "https://my.mainsail.xyz" ];
           };
         };
         description = lib.mdDoc ''
@@ -96,8 +97,18 @@ in {
   };
 
   config = mkIf cfg.enable {
-    warnings = optional (cfg.settings ? update_manager)
-      ''Enabling update_manager is not supported on NixOS and will lead to non-removable warnings in some clients.'';
+    warnings = []
+      ++ optional (cfg.settings ? update_manager)
+        ''Enabling update_manager is not supported on NixOS and will lead to non-removable warnings in some clients.''
+      ++ optional (cfg.configDir != null)
+        ''
+          services.moonraker.configDir has been deprecated upstream and will be removed.
+
+          Action: ${
+            if cfg.configDir == unifiedConfigDir then "Simply remove services.moonraker.configDir from your config."
+            else "Move files from `${cfg.configDir}` to `${unifiedConfigDir}` then remove services.moonraker.configDir from your config."
+          }
+        '';
 
     assertions = [
       {
@@ -124,20 +135,20 @@ in {
           port = cfg.port;
           klippy_uds_address = cfg.klipperSocket;
         };
+        machine = {
+          validate_service = false;
+        };
+      } // (lib.optionalAttrs (cfg.configDir != null) {
         file_manager = {
           config_path = cfg.configDir;
         };
-        database = {
-          database_path = "${cfg.stateDir}/database";
-        };
-      };
+      });
       fullConfig = recursiveUpdate cfg.settings forcedConfig;
     in format.generate "moonraker.cfg" fullConfig;
 
     systemd.tmpfiles.rules = [
       "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
-      "d '${cfg.configDir}' - ${cfg.user} ${cfg.group} - -"
-    ];
+    ] ++ lib.optional (cfg.configDir != null) "d '${cfg.configDir}' - ${cfg.user} ${cfg.group} - -";
 
     systemd.services.moonraker = {
       description = "Moonraker, an API web server for Klipper";
@@ -147,9 +158,16 @@ in {
 
       # Moonraker really wants its own config to be writable...
       script = ''
-        cp /etc/moonraker.cfg ${cfg.configDir}/moonraker-temp.cfg
-        chmod u+w ${cfg.configDir}/moonraker-temp.cfg
-        exec ${pkg}/bin/moonraker -c ${cfg.configDir}/moonraker-temp.cfg
+        config_path=${
+          # Deprecated separate config dir
+          if cfg.configDir != null then "${cfg.configDir}/moonraker-temp.cfg"
+          # Config in unified data path
+          else "${unifiedConfigDir}/moonraker-temp.cfg"
+        }
+        mkdir -p $(dirname "$config_path")
+        cp /etc/moonraker.cfg "$config_path"
+        chmod u+w "$config_path"
+        exec ${pkg}/bin/moonraker -d ${cfg.stateDir} -c "$config_path"
       '';
 
       # Needs `ip` command
@@ -184,5 +202,6 @@ in {
   meta.maintainers = with maintainers; [
     cab404
     vtuan10
+    zhaofengli
   ];
 }
diff --git a/nixos/modules/services/misc/n8n.nix b/nixos/modules/services/misc/n8n.nix
index f59df471e1e..2af37fba910 100644
--- a/nixos/modules/services/misc/n8n.nix
+++ b/nixos/modules/services/misc/n8n.nix
@@ -9,7 +9,6 @@ let
 in
 {
   options.services.n8n = {
-
     enable = mkEnableOption (lib.mdDoc "n8n server");
 
     openFirewall = mkOption {
@@ -22,11 +21,20 @@ in
       type = format.type;
       default = {};
       description = lib.mdDoc ''
-        Configuration for n8n, see <https://docs.n8n.io/reference/configuration.html>
+        Configuration for n8n, see <https://docs.n8n.io/hosting/environment-variables/configuration-methods/>
         for supported values.
       '';
     };
 
+    webhookUrl = mkOption {
+      type = types.str;
+      default = "";
+      description = lib.mdDoc ''
+        WEBHOOK_URL for n8n, in case we're running behind a reverse proxy.
+        This cannot be set through configuration and must reside in an environment variable.
+      '';
+    };
+
   };
 
   config = mkIf cfg.enable {
@@ -45,6 +53,11 @@ in
         N8N_USER_FOLDER = "/var/lib/n8n";
         HOME = "/var/lib/n8n";
         N8N_CONFIG_FILES = "${configFile}";
+        WEBHOOK_URL = "${cfg.webhookUrl}";
+
+        # Don't phone home
+        N8N_DIAGNOSTICS_ENABLED = "false";
+        N8N_VERSION_NOTIFICATIONS_ENABLED = "false";
       };
       serviceConfig = {
         Type = "simple";
diff --git a/nixos/modules/services/misc/nitter.nix b/nixos/modules/services/misc/nitter.nix
index 9e6ae189660..77f5459d117 100644
--- a/nixos/modules/services/misc/nitter.nix
+++ b/nixos/modules/services/misc/nitter.nix
@@ -45,6 +45,11 @@ let
   '';
 in
 {
+  imports = [
+    # https://github.com/zedeus/nitter/pull/772
+    (mkRemovedOptionModule [ "services" "nitter" "replaceInstagram" ] "Nitter no longer supports this option as Bibliogram has been discontinued.")
+  ];
+
   options = {
     services.nitter = {
       enable = mkEnableOption (lib.mdDoc "Nitter");
@@ -155,6 +160,22 @@ in
           description = lib.mdDoc "Use base64 encoding for proxied media URLs.";
         };
 
+        enableRSS = mkEnableOption (lib.mdDoc "RSS feeds") // { default = true; };
+
+        enableDebug = mkEnableOption (lib.mdDoc "request logs and debug endpoints");
+
+        proxy = mkOption {
+          type = types.str;
+          default = "";
+          description = lib.mdDoc "URL to a HTTP/HTTPS proxy.";
+        };
+
+        proxyAuth = mkOption {
+          type = types.str;
+          default = "";
+          description = lib.mdDoc "Credentials for proxy.";
+        };
+
         tokenCount = mkOption {
           type = types.int;
           default = 10;
@@ -185,10 +206,11 @@ in
           description = lib.mdDoc "Replace YouTube links with links to this instance (blank to disable).";
         };
 
-        replaceInstagram = mkOption {
+        replaceReddit = mkOption {
           type = types.str;
           default = "";
-          description = lib.mdDoc "Replace Instagram links with links to this instance (blank to disable).";
+          example = "teddit.net";
+          description = lib.mdDoc "Replace Reddit links with links to this instance (blank to disable).";
         };
 
         mp4Playback = mkOption {
@@ -268,6 +290,12 @@ in
           default = false;
           description = lib.mdDoc "Hide tweet replies.";
         };
+
+        squareAvatars = mkOption {
+          type = types.bool;
+          default = false;
+          description = lib.mdDoc "Square profile pictures.";
+        };
       };
 
       settings = mkOption {
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
deleted file mode 100644
index 1d115108c30..00000000000
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ /dev/null
@@ -1,837 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.nix;
-
-  nixPackage = cfg.package.out;
-
-  isNixAtLeast = versionAtLeast (getVersion nixPackage);
-
-  makeNixBuildUser = nr: {
-    name = "nixbld${toString nr}";
-    value = {
-      description = "Nix build user ${toString nr}";
-
-      /*
-        For consistency with the setgid(2), setuid(2), and setgroups(2)
-        calls in `libstore/build.cc', don't add any supplementary group
-        here except "nixbld".
-      */
-      uid = builtins.add config.ids.uids.nixbld nr;
-      isSystemUser = true;
-      group = "nixbld";
-      extraGroups = [ "nixbld" ];
-    };
-  };
-
-  nixbldUsers = listToAttrs (map makeNixBuildUser (range 1 cfg.nrBuildUsers));
-
-  nixConf =
-    assert isNixAtLeast "2.2";
-    let
-
-      mkValueString = v:
-        if v == null then ""
-        else if isInt v then toString v
-        else if isBool v then boolToString v
-        else if isFloat v then floatToString v
-        else if isList v then toString v
-        else if isDerivation v then toString v
-        else if builtins.isPath v then toString v
-        else if isString v then v
-        else if strings.isConvertibleWithToString v then toString v
-        else abort "The nix conf value: ${toPretty {} v} can not be encoded";
-
-      mkKeyValue = k: v: "${escape [ "=" ] k} = ${mkValueString v}";
-
-      mkKeyValuePairs = attrs: concatStringsSep "\n" (mapAttrsToList mkKeyValue attrs);
-
-    in
-    pkgs.writeTextFile {
-      name = "nix.conf";
-      text = ''
-        # WARNING: this file is generated from the nix.* options in
-        # your NixOS configuration, typically
-        # /etc/nixos/configuration.nix.  Do not edit it!
-        ${mkKeyValuePairs cfg.settings}
-        ${cfg.extraOptions}
-      '';
-      checkPhase = lib.optionalString cfg.checkConfig (
-        if pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform then ''
-          echo "Ignoring validation for cross-compilation"
-        ''
-        else ''
-          echo "Validating generated nix.conf"
-          ln -s $out ./nix.conf
-          set -e
-          set +o pipefail
-          NIX_CONF_DIR=$PWD \
-            ${cfg.package}/bin/nix show-config ${optionalString (isNixAtLeast "2.3pre") "--no-net"} \
-              ${optionalString (isNixAtLeast "2.4pre") "--option experimental-features nix-command"} \
-            |& sed -e 's/^warning:/error:/' \
-            | (! grep '${if cfg.checkAllErrors then "^error:" else "^error: unknown setting"}')
-          set -o pipefail
-        '');
-    };
-
-  legacyConfMappings = {
-    useSandbox = "sandbox";
-    buildCores = "cores";
-    maxJobs = "max-jobs";
-    sandboxPaths = "extra-sandbox-paths";
-    binaryCaches = "substituters";
-    trustedBinaryCaches = "trusted-substituters";
-    binaryCachePublicKeys = "trusted-public-keys";
-    autoOptimiseStore = "auto-optimise-store";
-    requireSignedBinaryCaches = "require-sigs";
-    trustedUsers = "trusted-users";
-    allowedUsers = "allowed-users";
-    systemFeatures = "system-features";
-  };
-
-  semanticConfType = with types;
-    let
-      confAtom = nullOr
-        (oneOf [
-          bool
-          int
-          float
-          str
-          path
-          package
-        ]) // {
-        description = "Nix config atom (null, bool, int, float, str, path or package)";
-      };
-    in
-    attrsOf (either confAtom (listOf confAtom));
-
-in
-
-{
-  imports = [
-    (mkRenamedOptionModuleWith { sinceRelease = 2003; from = [ "nix" "useChroot" ]; to = [ "nix" "useSandbox" ]; })
-    (mkRenamedOptionModuleWith { sinceRelease = 2003; from = [ "nix" "chrootDirs" ]; to = [ "nix" "sandboxPaths" ]; })
-    (mkRenamedOptionModuleWith { sinceRelease = 2205; from = [ "nix" "daemonIONiceLevel" ]; to = [ "nix" "daemonIOSchedPriority" ]; })
-    (mkRenamedOptionModuleWith { sinceRelease = 2211; from = [ "nix" "readOnlyStore" ]; to = [ "boot" "readOnlyNixStore" ]; })
-    (mkRemovedOptionModule [ "nix" "daemonNiceLevel" ] "Consider nix.daemonCPUSchedPolicy instead.")
-  ] ++ mapAttrsToList (oldConf: newConf: mkRenamedOptionModuleWith { sinceRelease = 2205; from = [ "nix" oldConf ]; to = [ "nix" "settings" newConf ]; }) legacyConfMappings;
-
-  ###### interface
-
-  options = {
-
-    nix = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Whether to enable Nix.
-          Disabling Nix makes the system hard to modify and the Nix programs and configuration will not be made available by NixOS itself.
-        '';
-      };
-
-      package = mkOption {
-        type = types.package;
-        default = pkgs.nix;
-        defaultText = literalExpression "pkgs.nix";
-        description = lib.mdDoc ''
-          This option specifies the Nix package instance to use throughout the system.
-        '';
-      };
-
-      distributedBuilds = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to distribute builds to the machines listed in
-          {option}`nix.buildMachines`.
-        '';
-      };
-
-      daemonCPUSchedPolicy = mkOption {
-        type = types.enum [ "other" "batch" "idle" ];
-        default = "other";
-        example = "batch";
-        description = lib.mdDoc ''
-          Nix daemon process CPU scheduling policy. This policy propagates to
-          build processes. `other` is the default scheduling
-          policy for regular tasks. The `batch` policy is
-          similar to `other`, but optimised for
-          non-interactive tasks. `idle` is for extremely
-          low-priority tasks that should only be run when no other task
-          requires CPU time.
-
-          Please note that while using the `idle` policy may
-          greatly improve responsiveness of a system performing expensive
-          builds, it may also slow down and potentially starve crucial
-          configuration updates during load.
-
-          `idle` may therefore be a sensible policy for
-          systems that experience only intermittent phases of high CPU load,
-          such as desktop or portable computers used interactively. Other
-          systems should use the `other` or
-          `batch` policy instead.
-
-          For more fine-grained resource control, please refer to
-          {manpage}`systemd.resource-control(5)` and adjust
-          {option}`systemd.services.nix-daemon` directly.
-      '';
-      };
-
-      daemonIOSchedClass = mkOption {
-        type = types.enum [ "best-effort" "idle" ];
-        default = "best-effort";
-        example = "idle";
-        description = lib.mdDoc ''
-          Nix daemon process I/O scheduling class. This class propagates to
-          build processes. `best-effort` is the default
-          class for regular tasks. The `idle` class is for
-          extremely low-priority tasks that should only perform I/O when no
-          other task does.
-
-          Please note that while using the `idle` scheduling
-          class can improve responsiveness of a system performing expensive
-          builds, it might also slow down or starve crucial configuration
-          updates during load.
-
-          `idle` may therefore be a sensible class for
-          systems that experience only intermittent phases of high I/O load,
-          such as desktop or portable computers used interactively. Other
-          systems should use the `best-effort` class.
-      '';
-      };
-
-      daemonIOSchedPriority = mkOption {
-        type = types.int;
-        default = 4;
-        example = 1;
-        description = lib.mdDoc ''
-          Nix daemon process I/O scheduling priority. This priority propagates
-          to build processes. The supported priorities depend on the
-          scheduling policy: With idle, priorities are not used in scheduling
-          decisions. best-effort supports values in the range 0 (high) to 7
-          (low).
-        '';
-      };
-
-      buildMachines = mkOption {
-        type = types.listOf (types.submodule {
-          options = {
-            hostName = mkOption {
-              type = types.str;
-              example = "nixbuilder.example.org";
-              description = lib.mdDoc ''
-                The hostname of the build machine.
-              '';
-            };
-            protocol = mkOption {
-              type = types.enum [ null "ssh" "ssh-ng" ];
-              default = "ssh";
-              example = "ssh-ng";
-              description = lib.mdDoc ''
-                The protocol used for communicating with the build machine.
-                Use `ssh-ng` if your remote builder and your
-                local Nix version support that improved protocol.
-
-                Use `null` when trying to change the special localhost builder
-                without a protocol which is for example used by hydra.
-              '';
-            };
-            system = mkOption {
-              type = types.nullOr types.str;
-              default = null;
-              example = "x86_64-linux";
-              description = lib.mdDoc ''
-                The system type the build machine can execute derivations on.
-                Either this attribute or {var}`systems` must be
-                present, where {var}`system` takes precedence if
-                both are set.
-              '';
-            };
-            systems = mkOption {
-              type = types.listOf types.str;
-              default = [ ];
-              example = [ "x86_64-linux" "aarch64-linux" ];
-              description = lib.mdDoc ''
-                The system types the build machine can execute derivations on.
-                Either this attribute or {var}`system` must be
-                present, where {var}`system` takes precedence if
-                both are set.
-              '';
-            };
-            sshUser = mkOption {
-              type = types.nullOr types.str;
-              default = null;
-              example = "builder";
-              description = lib.mdDoc ''
-                The username to log in as on the remote host. This user must be
-                able to log in and run nix commands non-interactively. It must
-                also be privileged to build derivations, so must be included in
-                {option}`nix.settings.trusted-users`.
-              '';
-            };
-            sshKey = mkOption {
-              type = types.nullOr types.str;
-              default = null;
-              example = "/root/.ssh/id_buildhost_builduser";
-              description = lib.mdDoc ''
-                The path to the SSH private key with which to authenticate on
-                the build machine. The private key must not have a passphrase.
-                If null, the building user (root on NixOS machines) must have an
-                appropriate ssh configuration to log in non-interactively.
-
-                Note that for security reasons, this path must point to a file
-                in the local filesystem, *not* to the nix store.
-              '';
-            };
-            maxJobs = mkOption {
-              type = types.int;
-              default = 1;
-              description = lib.mdDoc ''
-                The number of concurrent jobs the build machine supports. The
-                build machine will enforce its own limits, but this allows hydra
-                to schedule better since there is no work-stealing between build
-                machines.
-              '';
-            };
-            speedFactor = mkOption {
-              type = types.int;
-              default = 1;
-              description = lib.mdDoc ''
-                The relative speed of this builder. This is an arbitrary integer
-                that indicates the speed of this builder, relative to other
-                builders. Higher is faster.
-              '';
-            };
-            mandatoryFeatures = mkOption {
-              type = types.listOf types.str;
-              default = [ ];
-              example = [ "big-parallel" ];
-              description = lib.mdDoc ''
-                A list of features mandatory for this builder. The builder will
-                be ignored for derivations that don't require all features in
-                this list. All mandatory features are automatically included in
-                {var}`supportedFeatures`.
-              '';
-            };
-            supportedFeatures = mkOption {
-              type = types.listOf types.str;
-              default = [ ];
-              example = [ "kvm" "big-parallel" ];
-              description = lib.mdDoc ''
-                A list of features supported by this builder. The builder will
-                be ignored for derivations that require features not in this
-                list.
-              '';
-            };
-            publicHostKey = mkOption {
-              type = types.nullOr types.str;
-              default = null;
-              description = lib.mdDoc ''
-                The (base64-encoded) public host key of this builder. The field
-                is calculated via {command}`base64 -w0 /etc/ssh/ssh_host_type_key.pub`.
-                If null, SSH will use its regular known-hosts file when connecting.
-              '';
-            };
-          };
-        });
-        default = [ ];
-        description = lib.mdDoc ''
-          This option lists the machines to be used if distributed builds are
-          enabled (see {option}`nix.distributedBuilds`).
-          Nix will perform derivations on those machines via SSH by copying the
-          inputs to the Nix store on the remote machine, starting the build,
-          then copying the output back to the local Nix store.
-        '';
-      };
-
-      # Environment variables for running Nix.
-      envVars = mkOption {
-        type = types.attrs;
-        internal = true;
-        default = { };
-        description = lib.mdDoc "Environment variables used by Nix.";
-      };
-
-      nrBuildUsers = mkOption {
-        type = types.int;
-        description = lib.mdDoc ''
-          Number of `nixbld` user accounts created to
-          perform secure concurrent builds.  If you receive an error
-          message saying that “all build users are currently in use”,
-          you should increase this value.
-        '';
-      };
-
-      nixPath = mkOption {
-        type = types.listOf types.str;
-        default = [
-          "nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos"
-          "nixos-config=/etc/nixos/configuration.nix"
-          "/nix/var/nix/profiles/per-user/root/channels"
-        ];
-        description = lib.mdDoc ''
-          The default Nix expression search path, used by the Nix
-          evaluator to look up paths enclosed in angle brackets
-          (e.g. `<nixpkgs>`).
-        '';
-      };
-
-      checkConfig = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          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.
-        '';
-      };
-
-      registry = mkOption {
-        type = types.attrsOf (types.submodule (
-          let
-            referenceAttrs = with types; attrsOf (oneOf [
-              str
-              int
-              bool
-              path
-              package
-            ]);
-          in
-          { config, name, ... }:
-          {
-            options = {
-              from = mkOption {
-                type = referenceAttrs;
-                example = { type = "indirect"; id = "nixpkgs"; };
-                description = lib.mdDoc "The flake reference to be rewritten.";
-              };
-              to = mkOption {
-                type = referenceAttrs;
-                example = { type = "github"; owner = "my-org"; repo = "my-nixpkgs"; };
-                description = lib.mdDoc "The flake reference {option}`from` is rewritten to.";
-              };
-              flake = mkOption {
-                type = types.nullOr types.attrs;
-                default = null;
-                example = literalExpression "nixpkgs";
-                description = lib.mdDoc ''
-                  The flake input {option}`from` is rewritten to.
-                '';
-              };
-              exact = mkOption {
-                type = types.bool;
-                default = true;
-                description = lib.mdDoc ''
-                  Whether the {option}`from` reference needs to match exactly. If set,
-                  a {option}`from` reference like `nixpkgs` does not
-                  match with a reference like `nixpkgs/nixos-20.03`.
-                '';
-              };
-            };
-            config = {
-              from = mkDefault { type = "indirect"; id = name; };
-              to = mkIf (config.flake != null) (mkDefault (
-                {
-                  type = "path";
-                  path = config.flake.outPath;
-                } // filterAttrs
-                  (n: _: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash")
-                  config.flake
-              ));
-            };
-          }
-        ));
-        default = { };
-        description = lib.mdDoc ''
-          A system-wide flake registry.
-        '';
-      };
-
-      extraOptions = mkOption {
-        type = types.lines;
-        default = "";
-        example = ''
-          keep-outputs = true
-          keep-derivations = true
-        '';
-        description = lib.mdDoc "Additional text appended to {file}`nix.conf`.";
-      };
-
-      settings = mkOption {
-        type = types.submodule {
-          freeformType = semanticConfType;
-
-          options = {
-            max-jobs = mkOption {
-              type = types.either types.int (types.enum [ "auto" ]);
-              default = "auto";
-              example = 64;
-              description = lib.mdDoc ''
-                This option defines the maximum number of jobs that Nix will try to
-                build in parallel. The default is auto, which means it will use all
-                available logical cores. It is recommend to set it to the total
-                number of logical cores in your system (e.g., 16 for two CPUs with 4
-                cores each and hyper-threading).
-              '';
-            };
-
-            auto-optimise-store = mkOption {
-              type = types.bool;
-              default = false;
-              example = true;
-              description = lib.mdDoc ''
-                If set to true, Nix automatically detects files in the store that have
-                identical contents, and replaces them with hard links to a single copy.
-                This saves disk space. If set to false (the default), you can still run
-                nix-store --optimise to get rid of duplicate files.
-              '';
-            };
-
-            cores = mkOption {
-              type = types.int;
-              default = 0;
-              example = 64;
-              description = lib.mdDoc ''
-                This option defines the maximum number of concurrent tasks during
-                one build. It affects, e.g., -j option for make.
-                The special value 0 means that the builder should use all
-                available CPU cores in the system. Some builds may become
-                non-deterministic with this option; use with care! Packages will
-                only be affected if enableParallelBuilding is set for them.
-              '';
-            };
-
-            sandbox = mkOption {
-              type = types.either types.bool (types.enum [ "relaxed" ]);
-              default = true;
-              description = lib.mdDoc ''
-                If set, Nix will perform builds in a sandboxed environment that it
-                will set up automatically for each build. This prevents impurities
-                in builds by disallowing access to dependencies outside of the Nix
-                store by using network and mount namespaces in a chroot environment.
-                This is enabled by default even though it has a possible performance
-                impact due to the initial setup time of a sandbox for each build. It
-                doesn't affect derivation hashes, so changing this option will not
-                trigger a rebuild of packages.
-              '';
-            };
-
-            extra-sandbox-paths = mkOption {
-              type = types.listOf types.str;
-              default = [ ];
-              example = [ "/dev" "/proc" ];
-              description = lib.mdDoc ''
-                Directories from the host filesystem to be included
-                in the sandbox.
-              '';
-            };
-
-            substituters = mkOption {
-              type = types.listOf types.str;
-              description = lib.mdDoc ''
-                List of binary cache URLs used to obtain pre-built binaries
-                of Nix packages.
-
-                By default https://cache.nixos.org/ is added.
-              '';
-            };
-
-            trusted-substituters = mkOption {
-              type = types.listOf types.str;
-              default = [ ];
-              example = [ "https://hydra.nixos.org/" ];
-              description = lib.mdDoc ''
-                List of binary cache URLs that non-root users can use (in
-                addition to those specified using
-                {option}`nix.settings.substituters`) by passing
-                `--option binary-caches` to Nix commands.
-              '';
-            };
-
-            require-sigs = mkOption {
-              type = types.bool;
-              default = true;
-              description = lib.mdDoc ''
-                If enabled (the default), Nix will only download binaries from binary caches if
-                they are cryptographically signed with any of the keys listed in
-                {option}`nix.settings.trusted-public-keys`. If disabled, signatures are neither
-                required nor checked, so it's strongly recommended that you use only
-                trustworthy caches and https to prevent man-in-the-middle attacks.
-              '';
-            };
-
-            trusted-public-keys = mkOption {
-              type = types.listOf types.str;
-              example = [ "hydra.nixos.org-1:CNHJZBh9K4tP3EKF6FkkgeVYsS3ohTl+oS0Qa8bezVs=" ];
-              description = lib.mdDoc ''
-                List of public keys used to sign binary caches. If
-                {option}`nix.settings.trusted-public-keys` is enabled,
-                then Nix will use a binary from a binary cache if and only
-                if it is signed by *any* of the keys
-                listed here. By default, only the key for
-                `cache.nixos.org` is included.
-              '';
-            };
-
-            trusted-users = mkOption {
-              type = types.listOf types.str;
-              default = [ "root" ];
-              example = [ "root" "alice" "@wheel" ];
-              description = lib.mdDoc ''
-                A list of names of users that have additional rights when
-                connecting to the Nix daemon, such as the ability to specify
-                additional binary caches, or to import unsigned NARs. You
-                can also specify groups by prefixing them with
-                `@`; for instance,
-                `@wheel` means all users in the wheel
-                group.
-              '';
-            };
-
-            system-features = mkOption {
-              type = types.listOf types.str;
-              example = [ "kvm" "big-parallel" "gccarch-skylake" ];
-              description = lib.mdDoc ''
-                The set of features supported by the machine. Derivations
-                can express dependencies on system features through the
-                `requiredSystemFeatures` attribute.
-
-                By default, pseudo-features `nixos-test`, `benchmark`,
-                and `big-parallel` used in Nixpkgs are set, `kvm`
-                is also included if it is available.
-              '';
-            };
-
-            allowed-users = mkOption {
-              type = types.listOf types.str;
-              default = [ "*" ];
-              example = [ "@wheel" "@builders" "alice" "bob" ];
-              description = lib.mdDoc ''
-                A list of names of users (separated by whitespace) that are
-                allowed to connect to the Nix daemon. As with
-                {option}`nix.settings.trusted-users`, you can specify groups by
-                prefixing them with `@`. Also, you can
-                allow all users by specifying `*`. The
-                default is `*`. Note that trusted users are
-                always allowed to connect.
-              '';
-            };
-          };
-        };
-        default = { };
-        example = literalExpression ''
-          {
-            use-sandbox = true;
-            show-trace = true;
-
-            system-features = [ "big-parallel" "kvm" "recursive-nix" ];
-            sandbox-paths = { "/bin/sh" = "''${pkgs.busybox-sandbox-shell.out}/bin/busybox"; };
-          }
-        '';
-        description = lib.mdDoc ''
-          Configuration for Nix, see
-          <https://nixos.org/manual/nix/stable/#sec-conf-file> or
-          {manpage}`nix.conf(5)` for available options.
-          The value declared here will be translated directly to the key-value pairs Nix expects.
-
-          You can use {command}`nix-instantiate --eval --strict '<nixpkgs/nixos>' -A config.nix.settings`
-          to view the current value. By default it is empty.
-
-          Nix configurations defined under {option}`nix.*` will be translated and applied to this
-          option. In addition, configuration specified in {option}`nix.extraOptions` which will be appended
-          verbatim to the resulting config file.
-        '';
-      };
-    };
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-    environment.systemPackages =
-      [
-        nixPackage
-        pkgs.nix-info
-      ]
-      ++ optional (config.programs.bash.enableCompletion) pkgs.nix-bash-completions;
-
-    environment.etc."nix/nix.conf".source = nixConf;
-
-    environment.etc."nix/registry.json".text = builtins.toJSON {
-      version = 2;
-      flakes = mapAttrsToList (n: v: { inherit (v) from to exact; }) cfg.registry;
-    };
-
-    # List of machines for distributed Nix builds in the format
-    # expected by build-remote.pl.
-    environment.etc."nix/machines" = mkIf (cfg.buildMachines != [ ]) {
-      text =
-        concatMapStrings
-          (machine:
-            (concatStringsSep " " ([
-              "${optionalString (machine.protocol != null) "${machine.protocol}://"}${optionalString (machine.sshUser != null) "${machine.sshUser}@"}${machine.hostName}"
-              (if machine.system != null then machine.system else if machine.systems != [ ] then concatStringsSep "," machine.systems else "-")
-              (if machine.sshKey != null then machine.sshKey else "-")
-              (toString machine.maxJobs)
-              (toString machine.speedFactor)
-              (let res = (machine.supportedFeatures ++ machine.mandatoryFeatures);
-               in if (res == []) then "-" else (concatStringsSep "," res))
-              (let res = machine.mandatoryFeatures;
-               in if (res == []) then "-" else (concatStringsSep "," machine.mandatoryFeatures))
-            ]
-            ++ optional (isNixAtLeast "2.4pre") (if machine.publicHostKey != null then machine.publicHostKey else "-")))
-            + "\n"
-          )
-          cfg.buildMachines;
-    };
-
-    assertions =
-      let badMachine = m: m.system == null && m.systems == [ ];
-      in
-      [
-        {
-          assertion = !(any badMachine cfg.buildMachines);
-          message = ''
-            At least one system type (via <varname>system</varname> or
-              <varname>systems</varname>) must be set for every build machine.
-              Invalid machine specifications:
-          '' + "      " +
-          (concatStringsSep "\n      "
-            (map (m: m.hostName)
-              (filter (badMachine) cfg.buildMachines)));
-        }
-      ];
-
-    systemd.packages = [ nixPackage ];
-
-    # Will only work once https://github.com/NixOS/nix/pull/6285 is merged
-    # systemd.tmpfiles.packages = [ nixPackage ];
-
-    # Can be dropped for Nix > https://github.com/NixOS/nix/pull/6285
-    systemd.tmpfiles.rules = [
-      "d /nix/var/nix/daemon-socket 0755 root root - -"
-    ];
-
-    systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ];
-
-    systemd.services.nix-daemon =
-      {
-        path = [ nixPackage pkgs.util-linux config.programs.ssh.package ]
-          ++ optionals cfg.distributedBuilds [ pkgs.gzip ];
-
-        environment = cfg.envVars
-          // { CURL_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; }
-          // config.networking.proxy.envVars;
-
-        unitConfig.RequiresMountsFor = "/nix/store";
-
-        serviceConfig =
-          {
-            CPUSchedulingPolicy = cfg.daemonCPUSchedPolicy;
-            IOSchedulingClass = cfg.daemonIOSchedClass;
-            IOSchedulingPriority = cfg.daemonIOSchedPriority;
-            LimitNOFILE = 1048576;
-          };
-
-        restartTriggers = [ nixConf ];
-
-        # `stopIfChanged = false` changes to switch behavior
-        # from   stop -> update units -> start
-        #   to   update units -> restart
-        #
-        # The `stopIfChanged` setting therefore controls a trade-off between a
-        # more predictable lifecycle, which runs the correct "version" of
-        # the `ExecStop` line, and on the other hand the availability of
-        # sockets during the switch, as the effectiveness of the stop operation
-        # depends on the socket being stopped as well.
-        #
-        # As `nix-daemon.service` does not make use of `ExecStop`, we prefer
-        # to keep the socket up and available. This is important for machines
-        # that run Nix-based services, such as automated build, test, and deploy
-        # services, that expect the daemon socket to be available at all times.
-        #
-        # Notably, the Nix client does not retry on failure to connect to the
-        # daemon socket, and the in-process RemoteStore instance will disable
-        # itself. This makes retries infeasible even for services that are
-        # aware of the issue. Failure to connect can affect not only new client
-        # processes, but also new RemoteStore instances in existing processes,
-        # as well as existing RemoteStore instances that have not saturated
-        # their connection pool.
-        #
-        # Also note that `stopIfChanged = true` does not kill existing
-        # connection handling daemons, as one might wish to happen before a
-        # breaking Nix upgrade (which is rare). The daemon forks that handle
-        # the individual connections split off into their own sessions, causing
-        # them not to be stopped by systemd.
-        # If a Nix upgrade does require all existing daemon processes to stop,
-        # nix-daemon must do so on its own accord, and only when the new version
-        # starts and detects that Nix's persistent state needs an upgrade.
-        stopIfChanged = false;
-
-      };
-
-    # Set up the environment variables for running Nix.
-    environment.sessionVariables = cfg.envVars // { NIX_PATH = cfg.nixPath; };
-
-    environment.extraInit =
-      ''
-        if [ -e "$HOME/.nix-defexpr/channels" ]; then
-          export NIX_PATH="$HOME/.nix-defexpr/channels''${NIX_PATH:+:$NIX_PATH}"
-        fi
-      '';
-
-    nix.nrBuildUsers = mkDefault (
-      if cfg.settings.auto-allocate-uids or false then 0
-      else max 32 (if cfg.settings.max-jobs == "auto" then 0 else cfg.settings.max-jobs)
-    );
-
-    users.users = nixbldUsers;
-
-    services.xserver.displayManager.hiddenUsers = attrNames nixbldUsers;
-
-    system.activationScripts.nix = stringAfter [ "etc" "users" ]
-      ''
-        install -m 0755 -d /nix/var/nix/{gcroots,profiles}/per-user
-
-        # Subscribe the root user to the NixOS channel by default.
-        if [ ! -e "/root/.nix-channels" ]; then
-            echo "${config.system.defaultChannel} nixos" > "/root/.nix-channels"
-        fi
-      '';
-
-    # Legacy configuration conversion.
-    nix.settings = mkMerge [
-      {
-        trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ];
-        substituters = mkAfter [ "https://cache.nixos.org/" ];
-
-        system-features = mkDefault (
-          [ "nixos-test" "benchmark" "big-parallel" "kvm" ] ++
-          optionals (pkgs.hostPlatform ? gcc.arch) (
-            # a builder can run code for `gcc.arch` and inferior architectures
-            [ "gccarch-${pkgs.hostPlatform.gcc.arch}" ] ++
-            map (x: "gccarch-${x}") (systems.architectures.inferiors.${pkgs.hostPlatform.gcc.arch} or [])
-          )
-        );
-      }
-
-      (mkIf (!cfg.distributedBuilds) { builders = null; })
-
-      (mkIf (isNixAtLeast "2.3pre") { sandbox-fallback = false; })
-    ];
-
-  };
-
-}
diff --git a/nixos/modules/services/misc/nix-optimise.nix b/nixos/modules/services/misc/nix-optimise.nix
index db8148c060e..0398229a13d 100644
--- a/nixos/modules/services/misc/nix-optimise.nix
+++ b/nixos/modules/services/misc/nix-optimise.nix
@@ -1,28 +1,21 @@
 { config, lib, ... }:
 
-with lib;
-
 let
   cfg = config.nix.optimise;
 in
 
 {
-
-  ###### interface
-
   options = {
-
     nix.optimise = {
-
-      automatic = mkOption {
+      automatic = lib.mkOption {
         default = false;
-        type = types.bool;
+        type = lib.types.bool;
         description = lib.mdDoc "Automatically run the nix store optimiser at a specific time.";
       };
 
-      dates = mkOption {
+      dates = lib.mkOption {
         default = ["03:45"];
-        type = types.listOf types.str;
+        type = with lib.types; listOf str;
         description = lib.mdDoc ''
           Specification (in the format described by
           {manpage}`systemd.time(7)`) of the time at
@@ -32,9 +25,6 @@ in
     };
   };
 
-
-  ###### implementation
-
   config = {
     assertions = [
       {
@@ -43,14 +33,19 @@ in
       }
     ];
 
-    systemd.services.nix-optimise = lib.mkIf config.nix.enable
-      { description = "Nix Store Optimiser";
+    systemd = lib.mkIf config.nix.enable {
+      services.nix-optimise = {
+        description = "Nix Store Optimiser";
         # No point this if the nix daemon (and thus the nix store) is outside
         unitConfig.ConditionPathIsReadWrite = "/nix/var/nix/daemon-socket";
         serviceConfig.ExecStart = "${config.nix.package}/bin/nix-store --optimise";
-        startAt = optionals cfg.automatic cfg.dates;
+        startAt = lib.optionals cfg.automatic cfg.dates;
       };
 
+      timers.nix-optimise.timerConfig = {
+        Persistent = true;
+        RandomizedDelaySec = 1800;
+      };
+    };
   };
-
 }
diff --git a/nixos/modules/services/misc/ntfy-sh.nix b/nixos/modules/services/misc/ntfy-sh.nix
index 9d52fcf2536..8fc1df93afb 100644
--- a/nixos/modules/services/misc/ntfy-sh.nix
+++ b/nixos/modules/services/misc/ntfy-sh.nix
@@ -32,7 +32,25 @@ in
     };
 
     settings = mkOption {
-      type = types.submodule { freeformType = settingsFormat.type; };
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+        options = {
+          base-url = mkOption {
+            type = types.str;
+            example = "https://ntfy.example";
+            description = lib.mdDoc ''
+              Public facing base URL of the service
+
+              This setting is required for any of the following features:
+              - attachments (to return a download URL)
+              - e-mail sending (for the topic URL in the email footer)
+              - iOS push notifications for self-hosted servers
+                (to calculate the Firebase poll_request topic)
+              - Matrix Push Gateway (to validate that the pushkey is correct)
+            '';
+          };
+        };
+      };
 
       default = { };
 
@@ -59,6 +77,19 @@ in
         systemPackages = [ cfg.package ];
       };
 
+      services.ntfy-sh.settings = {
+        auth-file = mkDefault "/var/lib/ntfy-sh/user.db";
+        listen-http = mkDefault "127.0.0.1:2586";
+        attachment-cache-dir = mkDefault "/var/lib/ntfy-sh/attachments";
+        cache-file = mkDefault "/var/lib/ntfy-sh/cache-file.db";
+      };
+
+      systemd.tmpfiles.rules = [
+        "f ${cfg.settings.auth-file} 0600 ${cfg.user} ${cfg.group} - -"
+        "d ${cfg.settings.attachment-cache-dir} 0700 ${cfg.user} ${cfg.group} - -"
+        "f ${cfg.settings.cache-file} 0600 ${cfg.user} ${cfg.group} - -"
+      ];
+
       systemd.services.ntfy-sh = {
         description = "Push notifications server";
 
@@ -68,7 +99,9 @@ in
         serviceConfig = {
           ExecStart = "${cfg.package}/bin/ntfy serve -c ${configuration}";
           User = cfg.user;
+          StateDirectory = "ntfy-sh";
 
+          DynamicUser = true;
           AmbientCapabilities = "CAP_NET_BIND_SERVICE";
           PrivateTmp = true;
           NoNewPrivileges = true;
@@ -83,6 +116,8 @@ in
           RestrictNamespaces = true;
           RestrictRealtime = true;
           MemoryDenyWriteExecute = true;
+          # Upstream Recommandation
+          LimitNOFILE = 20500;
         };
       };
 
diff --git a/nixos/modules/services/misc/octoprint.nix b/nixos/modules/services/misc/octoprint.nix
index c216c6fa2b7..43e0ce0c21d 100644
--- a/nixos/modules/services/misc/octoprint.nix
+++ b/nixos/modules/services/misc/octoprint.nix
@@ -106,6 +106,9 @@ in
 
     systemd.tmpfiles.rules = [
       "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
+      # this will allow octoprint access to raspberry specific hardware to check for throttling
+      # read-only will not work: "VCHI initialization failed" error
+      "a /dev/vchiq - - - - u:octoprint:rw"
     ];
 
     systemd.services.octoprint = {
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index 1dddd147ac0..8fe628a4088 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -6,6 +6,7 @@ let
   pkg = cfg.package;
 
   defaultUser = "paperless";
+  nltkDir = "/var/cache/paperless/nltk";
 
   # Don't start a redis instance if the user sets a custom redis connection
   enableRedis = !hasAttr "PAPERLESS_REDIS" cfg.extraConfig;
@@ -15,6 +16,7 @@ let
     PAPERLESS_DATA_DIR = cfg.dataDir;
     PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
     PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
+    PAPERLESS_NLTK_DIR = nltkDir;
     GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
   } // optionalAttrs (config.time.timeZone != null) {
     PAPERLESS_TIME_ZONE = config.time.timeZone;
@@ -24,12 +26,14 @@ let
     lib.mapAttrs (_: toString) cfg.extraConfig
   );
 
-  manage = let
-    setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
-  in pkgs.writeShellScript "manage" ''
-    ${setupEnv}
-    exec ${pkg}/bin/paperless-ngx "$@"
-  '';
+  manage =
+    let
+      setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
+    in
+    pkgs.writeShellScript "manage" ''
+      ${setupEnv}
+      exec ${pkg}/bin/paperless-ngx "$@"
+    '';
 
   # Secure the services
   defaultServiceConfig = {
@@ -47,6 +51,7 @@ let
       cfg.dataDir
       cfg.mediaDir
     ];
+    CacheDirectory = "paperless";
     CapabilityBoundingSet = "";
     # ProtectClock adds DeviceAllow=char-rtc r
     DeviceAllow = "";
@@ -81,12 +86,11 @@ let
     SupplementaryGroups = optional enableRedis redisServer.user;
     SystemCallArchitectures = "native";
     SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ];
-    # Does not work well with the temporary root
-    #UMask = "0066";
+    UMask = "0066";
   };
 in
 {
-  meta.maintainers = with maintainers; [ erikarvstedt Flakebi ];
+  meta.maintainers = with maintainers; [ erikarvstedt Flakebi leona ];
 
   imports = [
     (mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ])
@@ -170,7 +174,7 @@ in
 
     extraConfig = mkOption {
       type = types.attrs;
-      default = {};
+      default = { };
       description = lib.mdDoc ''
         Extra paperless config options.
 
@@ -226,9 +230,26 @@ in
 
         # Auto-migrate on first run or if the package has changed
         versionFile="${cfg.dataDir}/src-version"
-        if [[ $(cat "$versionFile" 2>/dev/null) != ${pkg} ]]; then
+        version=$(cat "$versionFile" 2>/dev/null || echo 0)
+
+        if [[ $version != ${pkg.version} ]]; then
           ${pkg}/bin/paperless-ngx migrate
-          echo ${pkg} > "$versionFile"
+
+          # Parse old version string format for backwards compatibility
+          version=$(echo "$version" | grep -ohP '[^-]+$')
+
+          versionLessThan() {
+            target=$1
+            [[ $({ echo "$version"; echo "$target"; } | sort -V | head -1) != "$target" ]]
+          }
+
+          if versionLessThan 1.12.0; then
+            # Reindex documents as mentioned in https://github.com/paperless-ngx/paperless-ngx/releases/tag/v1.12.1
+            echo "Reindexing documents, to allow searching old comments. Required after the 1.12.x upgrade."
+            ${pkg}/bin/paperless-ngx document_index reindex
+          fi
+
+          echo ${pkg.version} > "$versionFile"
         fi
       ''
       + optionalString (cfg.passwordFile != null) ''
@@ -274,6 +295,33 @@ in
       };
     };
 
+    # Download NLTK corpus data
+    systemd.services.paperless-download-nltk-data = {
+      wantedBy = [ "paperless-scheduler.service" ];
+      before = [ "paperless-scheduler.service" ];
+      after = [ "network-online.target" ];
+      serviceConfig = defaultServiceConfig // {
+        User = cfg.user;
+        Type = "oneshot";
+        # Enable internet access
+        PrivateNetwork = false;
+        # Restrict write access
+        BindPaths = [];
+        BindReadOnlyPaths = [
+          "/nix/store"
+          "-/etc/resolv.conf"
+          "-/etc/nsswitch.conf"
+          "-/etc/ssl/certs"
+          "-/etc/static/ssl/certs"
+          "-/etc/hosts"
+          "-/etc/localtime"
+        ];
+        ExecStart = let pythonWithNltk = pkg.python.withPackages (ps: [ ps.nltk ]); in ''
+          ${pythonWithNltk}/bin/python -m nltk.downloader -d '${nltkDir}' punkt snowball_data stopwords
+        '';
+      };
+    };
+
     systemd.services.paperless-consumer = {
       description = "Paperless document consumer";
       # Bind to `paperless-scheduler` so that the consumer never runs
diff --git a/nixos/modules/services/misc/polaris.nix b/nixos/modules/services/misc/polaris.nix
index 83da486083b..70f097f0284 100644
--- a/nixos/modules/services/misc/polaris.nix
+++ b/nixos/modules/services/misc/polaris.nix
@@ -13,7 +13,7 @@ in
     services.polaris = {
       enable = mkEnableOption (lib.mdDoc "Polaris Music Server");
 
-      package = mkPackageOption pkgs "polaris" { };
+      package = mkPackageOptionMD pkgs "polaris" { };
 
       user = mkOption {
         type = types.str;
diff --git a/nixos/modules/services/misc/portunus.nix b/nixos/modules/services/misc/portunus.nix
index f60cbe34771..d1888198697 100644
--- a/nixos/modules/services/misc/portunus.nix
+++ b/nixos/modules/services/misc/portunus.nix
@@ -107,8 +107,9 @@ in
     ldap = {
       package = mkOption {
         type = types.package;
-        default = pkgs.openldap;
-        defaultText = lib.literalExpression "pkgs.openldap";
+        # needs openldap built with a libxcrypt that support crypt sha256 until https://github.com/majewsky/portunus/issues/2 is solved
+        default = pkgs.openldap.override { libxcrypt = pkgs.libxcrypt-legacy; };
+        defaultText = lib.literalExpression "pkgs.openldap.override { libxcrypt = pkgs.libxcrypt-legacy; }";
         description = lib.mdDoc "The OpenLDAP package to use.";
       };
 
@@ -238,7 +239,7 @@ in
           PORTUNUS_SERVER_BINARY = "${cfg.package}/bin/portunus-server";
           PORTUNUS_SERVER_GROUP = cfg.group;
           PORTUNUS_SERVER_USER = cfg.user;
-          PORTUNUS_SERVER_HTTP_LISTEN = "[::]:${toString cfg.port}";
+          PORTUNUS_SERVER_HTTP_LISTEN = "127.0.0.1:${toString cfg.port}";
           PORTUNUS_SERVER_STATE_DIR = cfg.stateDir;
           PORTUNUS_SLAPD_BINARY = "${cfg.ldap.package}/libexec/slapd";
           PORTUNUS_SLAPD_GROUP = cfg.ldap.group;
diff --git a/nixos/modules/services/misc/pufferpanel.nix b/nixos/modules/services/misc/pufferpanel.nix
new file mode 100644
index 00000000000..2022406c832
--- /dev/null
+++ b/nixos/modules/services/misc/pufferpanel.nix
@@ -0,0 +1,176 @@
+{ config, pkgs, lib, ... }:
+let
+  cfg = config.services.pufferpanel;
+in
+{
+  options.services.pufferpanel = {
+    enable = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Whether to enable PufferPanel game management server.
+
+        Note that [PufferPanel templates] and binaries downloaded by PufferPanel
+        expect [FHS environment]. It is possible to set {option}`package` option
+        to use PufferPanel wrapper with FHS environment. For example, to use
+        `Download Game from Steam` and `Download Java` template operations:
+        ```Nix
+        { lib, pkgs, ... }: {
+          services.pufferpanel = {
+            enable = true;
+            extraPackages = with pkgs; [ bash curl gawk gnutar gzip ];
+            package = pkgs.buildFHSEnv {
+              name = "pufferpanel-fhs";
+              runScript = lib.getExe pkgs.pufferpanel;
+              targetPkgs = pkgs': with pkgs'; [ icu openssl zlib ];
+            };
+          };
+        }
+        ```
+
+        [PufferPanel templates]: https://github.com/PufferPanel/templates
+        [FHS environment]: https://wikipedia.org/wiki/Filesystem_Hierarchy_Standard
+      '';
+    };
+
+    package = lib.mkPackageOptionMD pkgs "pufferpanel" { };
+
+    extraGroups = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
+      default = [ ];
+      example = [ "podman" ];
+      description = lib.mdDoc ''
+        Additional groups for the systemd service.
+      '';
+    };
+
+    extraPackages = lib.mkOption {
+      type = lib.types.listOf lib.types.package;
+      default = [ ];
+      example = lib.literalExpression "[ pkgs.jre ]";
+      description = lib.mdDoc ''
+        Packages to add to the PATH environment variable. Both the {file}`bin`
+        and {file}`sbin` subdirectories of each package are added.
+      '';
+    };
+
+    environment = lib.mkOption {
+      type = lib.types.attrsOf lib.types.str;
+      default = { };
+      example = lib.literalExpression ''
+        {
+          PUFFER_WEB_HOST = ":8080";
+          PUFFER_DAEMON_SFTP_HOST = ":5657";
+          PUFFER_DAEMON_CONSOLE_BUFFER = "1000";
+          PUFFER_DAEMON_CONSOLE_FORWARD = "true";
+          PUFFER_PANEL_REGISTRATIONENABLED = "false";
+        }
+      '';
+      description = lib.mdDoc ''
+        Environment variables to set for the service. Secrets should be
+        specified using {option}`environmentFile`.
+
+        Refer to the [PufferPanel source code][] for the list of available
+        configuration options. Variable name is an upper-cased configuration
+        entry name with underscores instead of dots, prefixed with `PUFFER_`.
+        For example, `panel.settings.companyName` entry can be set using
+        {env}`PUFFER_PANEL_SETTINGS_COMPANYNAME`.
+
+        When running with panel enabled (configured with `PUFFER_PANEL_ENABLE`
+        environment variable), it is recommended disable registration using
+        `PUFFER_PANEL_REGISTRATIONENABLED` environment variable (registration is
+        enabled by default). To create the initial administrator user, run
+        {command}`pufferpanel --workDir /var/lib/pufferpanel user add --admin`.
+
+        Some options override corresponding settings set via web interface (e.g.
+        `PUFFER_PANEL_REGISTRATIONENABLED`). Those options can be temporarily
+        toggled or set in settings but do not persist between restarts.
+
+        [PufferPanel source code]: https://github.com/PufferPanel/PufferPanel/blob/master/config/entries.go
+      '';
+    };
+
+    environmentFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = lib.mdDoc ''
+        File to load environment variables from. Loaded variables override
+        values set in {option}`environment`.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.pufferpanel = {
+      description = "PufferPanel game management server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      path = cfg.extraPackages;
+      environment = cfg.environment;
+
+      # Note that we export environment variables for service directories if the
+      # value is not set. An empty environment variable is considered to be set.
+      # E.g.
+      #   export PUFFER_LOGS=${PUFFER_LOGS-$LOGS_DIRECTORY}
+      # would set PUFFER_LOGS to $LOGS_DIRECTORY if PUFFER_LOGS environment
+      # variable is not defined.
+      script = ''
+        ${lib.concatLines (lib.mapAttrsToList (name: value: ''
+          export ${name}="''${${name}-${value}}"
+        '') {
+          PUFFER_LOGS = "$LOGS_DIRECTORY";
+          PUFFER_DAEMON_DATA_CACHE = "$CACHE_DIRECTORY";
+          PUFFER_DAEMON_DATA_SERVERS = "$STATE_DIRECTORY/servers";
+          PUFFER_DAEMON_DATA_BINARIES = "$STATE_DIRECTORY/binaries";
+        })}
+        exec ${lib.getExe cfg.package} run --workDir "$STATE_DIRECTORY"
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        UMask = "0077";
+
+        SupplementaryGroups = cfg.extraGroups;
+
+        StateDirectory = "pufferpanel";
+        StateDirectoryMode = "0700";
+        CacheDirectory = "pufferpanel";
+        CacheDirectoryMode = "0700";
+        LogsDirectory = "pufferpanel";
+        LogsDirectoryMode = "0700";
+
+        EnvironmentFile = cfg.environmentFile;
+
+        # Command "pufferpanel shutdown --pid $MAINPID" sends SIGTERM (code 15)
+        # to the main process and waits for termination. This is essentially
+        # KillMode=mixed we are using here. See
+        # https://freedesktop.org/software/systemd/man/systemd.kill.html#KillMode=
+        KillMode = "mixed";
+
+        DynamicUser = true;
+        ProtectHome = true;
+        ProtectProc = "invisible";
+        ProtectClock = true;
+        ProtectHostname = true;
+        ProtectControlGroups = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        PrivateUsers = true;
+        PrivateDevices = true;
+        RestrictRealtime = true;
+        RestrictNamespaces = [ "user" "mnt" ]; # allow buildFHSEnv
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        LockPersonality = true;
+        DeviceAllow = [ "" ];
+        DevicePolicy = "closed";
+        CapabilityBoundingSet = [ "" ];
+      };
+    };
+  };
+
+  meta.maintainers = [ lib.maintainers.tie ];
+}
diff --git a/nixos/modules/services/misc/pykms.nix b/nixos/modules/services/misc/pykms.nix
index 314388e0152..be3accc0d7e 100644
--- a/nixos/modules/services/misc/pykms.nix
+++ b/nixos/modules/services/misc/pykms.nix
@@ -85,7 +85,7 @@ in
         WorkingDirectory = libDir;
         SyslogIdentifier = "pykms";
         Restart = "on-failure";
-        MemoryLimit = cfg.memoryLimit;
+        MemoryMax = cfg.memoryLimit;
       };
     };
   };
diff --git a/nixos/modules/services/misc/readarr.nix b/nixos/modules/services/misc/readarr.nix
new file mode 100644
index 00000000000..dd4fef6e598
--- /dev/null
+++ b/nixos/modules/services/misc/readarr.nix
@@ -0,0 +1,88 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.readarr;
+in
+{
+  options = {
+    services.readarr = {
+      enable = mkEnableOption (lib.mdDoc "Readarr");
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/readarr/";
+        description = lib.mdDoc "The directory where Readarr stores its data files.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.readarr;
+        defaultText = literalExpression "pkgs.readarr";
+        description = lib.mdDoc "The Readarr package to use";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Open ports in the firewall for Readarr
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "readarr";
+        description = lib.mdDoc ''
+          User account under which Readarr runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "readarr";
+        description = lib.mdDoc ''
+          Group under which Readarr runs.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.readarr = {
+      description = "Readarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/Readarr -nobrowser -data='${cfg.dataDir}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 8787 ];
+    };
+
+    users.users = mkIf (cfg.user == "readarr") {
+      readarr = {
+        description = "Readarr service";
+        home = cfg.dataDir;
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "readarr") {
+      readarr = { };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 58a595b5c76..a296fd3816b 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -283,13 +283,13 @@ in
 
     services.redmine.settings = {
       production = {
-        scm_subversion_command = if cfg.components.subversion then "${pkgs.subversion}/bin/svn" else "";
-        scm_mercurial_command = if cfg.components.mercurial then "${pkgs.mercurial}/bin/hg" else "";
-        scm_git_command = if cfg.components.git then "${pkgs.git}/bin/git" else "";
-        scm_cvs_command = if cfg.components.cvs then "${pkgs.cvs}/bin/cvs" else "";
-        scm_bazaar_command = if cfg.components.breezy then "${pkgs.breezy}/bin/bzr" else "";
-        imagemagick_convert_command = if cfg.components.imagemagick then "${pkgs.imagemagick}/bin/convert" else "";
-        gs_command = if cfg.components.ghostscript then "${pkgs.ghostscript}/bin/gs" else "";
+        scm_subversion_command = optionalString cfg.components.subversion "${pkgs.subversion}/bin/svn";
+        scm_mercurial_command = optionalString cfg.components.mercurial "${pkgs.mercurial}/bin/hg";
+        scm_git_command = optionalString cfg.components.git "${pkgs.git}/bin/git";
+        scm_cvs_command = optionalString cfg.components.cvs "${pkgs.cvs}/bin/cvs";
+        scm_bazaar_command = optionalString cfg.components.breezy "${pkgs.breezy}/bin/bzr";
+        imagemagick_convert_command = optionalString cfg.components.imagemagick "${pkgs.imagemagick}/bin/convert";
+        gs_command = optionalString cfg.components.ghostscript "${pkgs.ghostscript}/bin/gs";
         minimagick_font_path = "${cfg.components.minimagick_font_path}";
       };
     };
@@ -419,7 +419,7 @@ in
         Group = cfg.group;
         TimeoutSec = "300";
         WorkingDirectory = "${cfg.package}/share/redmine";
-        ExecStart="${bundle} exec rails server webrick -e production -p ${toString cfg.port} -P '${cfg.stateDir}/redmine.pid'";
+        ExecStart="${bundle} exec rails server -u webrick -e production -p ${toString cfg.port} -P '${cfg.stateDir}/redmine.pid'";
       };
 
     };
diff --git a/nixos/modules/services/misc/rshim.nix b/nixos/modules/services/misc/rshim.nix
new file mode 100644
index 00000000000..0fef2cc228c
--- /dev/null
+++ b/nixos/modules/services/misc/rshim.nix
@@ -0,0 +1,99 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.rshim;
+
+  rshimCommand = [ "${cfg.package}/bin/rshim" ]
+    ++ lib.optionals (cfg.backend != null) [ "--backend ${cfg.backend}" ]
+    ++ lib.optionals (cfg.device != null) [ "--device ${cfg.device}" ]
+    ++ lib.optionals (cfg.index != null) [ "--index ${builtins.toString cfg.index}" ]
+    ++ [ "--log-level ${builtins.toString cfg.log-level}" ]
+  ;
+in
+{
+  options.services.rshim = {
+    enable = lib.mkEnableOption (lib.mdDoc "User-space rshim driver for the BlueField SoC");
+
+    package = lib.mkPackageOptionMD pkgs "rshim-user-space" { };
+
+    backend = lib.mkOption {
+      type = with lib.types; nullOr (enum [ "usb" "pcie" "pcie_lf" ]);
+      description = lib.mdDoc ''
+        Specify the backend to attach. If not specified, the driver will scan
+        all rshim backends unless the `device` option is given with a device
+        name specified.
+      '';
+      default = null;
+      example = "pcie";
+    };
+
+    device = lib.mkOption {
+      type = with lib.types; nullOr str;
+      description = lib.mdDoc ''
+        Specify the device name to attach. The backend driver can be deduced
+        from the device name, thus the `backend` option is not needed.
+      '';
+      default = null;
+      example = "pcie-04:00.2";
+    };
+
+    index = lib.mkOption {
+      type = with lib.types; nullOr int;
+      description = lib.mdDoc ''
+        Specify the index to create device path `/dev/rshim<index>`. It's also
+        used to create network interface name `tmfifo_net<index>`. This option
+        is needed when multiple rshim instances are running.
+      '';
+      default = null;
+      example = 1;
+    };
+
+    log-level = lib.mkOption {
+      type = lib.types.int;
+      description = lib.mdDoc ''
+        Specify the log level (0:none, 1:error, 2:warning, 3:notice, 4:debug).
+      '';
+      default = 2;
+      example = 4;
+    };
+
+    config = lib.mkOption {
+      type = with lib.types; attrsOf (oneOf [ int str ]);
+      description = lib.mdDoc ''
+        Structural setting for the rshim configuration file
+        (`/etc/rshim.conf`). It can be used to specify the static mapping
+        between rshim devices and rshim names. It can also be used to ignore
+        some rshim devices.
+      '';
+      default = { };
+      example = {
+        DISPLAY_LEVEL = 0;
+        rshim0 = "usb-2-1.7";
+        none = "usb-1-1.4";
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.etc = lib.mkIf (cfg.config != { }) {
+      "rshim.conf".text = lib.generators.toKeyValue
+        { mkKeyValue = lib.generators.mkKeyValueDefault { } " "; }
+        cfg.config;
+    };
+
+    systemd.services.rshim = {
+      after = [ "network.target" ];
+      serviceConfig = {
+        Restart = "always";
+        Type = "forking";
+        ExecStart = [
+          (lib.concatStringsSep " \\\n" rshimCommand)
+        ];
+        KillMode = "control-group";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ nikstur ];
+}
diff --git a/nixos/modules/services/misc/siproxd.nix b/nixos/modules/services/misc/siproxd.nix
index f1a1ed4d29b..3890962b7cf 100644
--- a/nixos/modules/services/misc/siproxd.nix
+++ b/nixos/modules/services/misc/siproxd.nix
@@ -20,7 +20,7 @@ let
     ${optionalString (cfg.hostsAllowReg != []) "hosts_allow_reg = ${concatStringsSep "," cfg.hostsAllowReg}"}
     ${optionalString (cfg.hostsAllowSip != []) "hosts_allow_sip = ${concatStringsSep "," cfg.hostsAllowSip}"}
     ${optionalString (cfg.hostsDenySip != []) "hosts_deny_sip  = ${concatStringsSep "," cfg.hostsDenySip}"}
-    ${if (cfg.passwordFile != "") then "proxy_auth_pwfile = ${cfg.passwordFile}" else ""}
+    ${optionalString (cfg.passwordFile != "") "proxy_auth_pwfile = ${cfg.passwordFile}"}
     ${cfg.extraConfig}
   '';
 
@@ -60,7 +60,7 @@ in
         default = [ ];
         example = [ "192.168.1.0/24" "192.168.2.0/24" ];
         description = lib.mdDoc ''
-          Acess control list for incoming SIP registrations.
+          Access control list for incoming SIP registrations.
         '';
       };
 
@@ -69,7 +69,7 @@ in
         default = [ ];
         example = [ "123.45.0.0/16" "123.46.0.0/16" ];
         description = lib.mdDoc ''
-          Acess control list for incoming SIP traffic.
+          Access control list for incoming SIP traffic.
         '';
       };
 
@@ -78,7 +78,7 @@ in
         default = [ ];
         example = [ "10.0.0.0/8" "11.0.0.0/8" ];
         description = lib.mdDoc ''
-          Acess control list for denying incoming
+          Access control list for denying incoming
           SIP registrations and traffic.
         '';
       };
diff --git a/nixos/modules/services/misc/snapper.nix b/nixos/modules/services/misc/snapper.nix
index cfdfa2830ce..569433c3c71 100644
--- a/nixos/modules/services/misc/snapper.nix
+++ b/nixos/modules/services/misc/snapper.nix
@@ -4,6 +4,81 @@ with lib;
 
 let
   cfg = config.services.snapper;
+
+  mkValue = v:
+    if isList v then "\"${concatMapStringsSep " " (escape [ "\\" " " ]) v}\""
+    else if v == true then "yes"
+    else if v == false then "no"
+    else if isString v then "\"${v}\""
+    else builtins.toJSON v;
+
+  mkKeyValue = k: v: "${k}=${mkValue v}";
+
+  # "it's recommended to always specify the filesystem type"  -- man snapper-configs
+  defaultOf = k: if k == "FSTYPE" then null else configOptions.${k}.default or null;
+
+  safeStr = types.strMatching "[^\n\"]*" // {
+    description = "string without line breaks or quotes";
+    descriptionClass = "conjunction";
+  };
+
+  configOptions = {
+    SUBVOLUME = mkOption {
+      type = types.path;
+      description = lib.mdDoc ''
+        Path of the subvolume or mount point.
+        This path is a subvolume and has to contain a subvolume named
+        .snapshots.
+        See also man:snapper(8) section PERMISSIONS.
+      '';
+    };
+
+    FSTYPE = mkOption {
+      type = types.enum [ "btrfs" ];
+      default = "btrfs";
+      description = lib.mdDoc ''
+        Filesystem type. Only btrfs is stable and tested.
+      '';
+    };
+
+    ALLOW_GROUPS = mkOption {
+      type = types.listOf safeStr;
+      default = [];
+      description = lib.mdDoc ''
+        List of groups allowed to operate with the config.
+
+        Also see the PERMISSIONS section in man:snapper(8).
+      '';
+    };
+
+    ALLOW_USERS = mkOption {
+      type = types.listOf safeStr;
+      default = [];
+      example = [ "alice" ];
+      description = lib.mdDoc ''
+        List of users allowed to operate with the config. "root" is always
+        implicitly included.
+
+        Also see the PERMISSIONS section in man:snapper(8).
+      '';
+    };
+
+    TIMELINE_CLEANUP = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Defines whether the timeline cleanup algorithm should be run for the config.
+      '';
+    };
+
+    TIMELINE_CREATE = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Defines whether hourly snapshots should be created.
+      '';
+    };
+  };
 in
 
 {
@@ -52,49 +127,23 @@ in
       example = literalExpression ''
         {
           home = {
-            subvolume = "/home";
-            extraConfig = '''
-              ALLOW_USERS="alice"
-              TIMELINE_CREATE=yes
-              TIMELINE_CLEANUP=yes
-            ''';
+            SUBVOLUME = "/home";
+            ALLOW_USERS = [ "alice" ];
+            TIMELINE_CREATE = true;
+            TIMELINE_CLEANUP = true;
           };
         }
       '';
 
       description = lib.mdDoc ''
-        Subvolume configuration
+        Subvolume configuration. Any option mentioned in man:snapper-configs(5)
+        is valid here, even if NixOS doesn't document it.
       '';
 
       type = types.attrsOf (types.submodule {
-        options = {
-          subvolume = mkOption {
-            type = types.path;
-            description = lib.mdDoc ''
-              Path of the subvolume or mount point.
-              This path is a subvolume and has to contain a subvolume named
-              .snapshots.
-              See also man:snapper(8) section PERMISSIONS.
-            '';
-          };
-
-          fstype = mkOption {
-            type = types.enum [ "btrfs" ];
-            default = "btrfs";
-            description = lib.mdDoc ''
-              Filesystem type. Only btrfs is stable and tested.
-            '';
-          };
+        freeformType = types.attrsOf (types.oneOf [ (types.listOf safeStr) types.bool safeStr types.number ]);
 
-          extraConfig = mkOption {
-            type = types.lines;
-            default = "";
-            description = lib.mdDoc ''
-              Additional configuration next to SUBVOLUME and FSTYPE.
-              See man:snapper-configs(5).
-            '';
-          };
-        };
+        options = configOptions;
       });
     };
   };
@@ -117,11 +166,7 @@ in
 
       }
       // (mapAttrs' (name: subvolume: nameValuePair "snapper/configs/${name}" ({
-        text = ''
-          ${subvolume.extraConfig}
-          FSTYPE="${subvolume.fstype}"
-          SUBVOLUME="${subvolume.subvolume}"
-        '';
+        text = lib.generators.toKeyValue { inherit mkKeyValue; } (filterAttrs (k: v: v != defaultOf k) subvolume);
       })) cfg.configs)
       // (lib.optionalAttrs (cfg.filters != null) {
         "snapper/filters/default.txt".text = cfg.filters;
@@ -175,11 +220,34 @@ in
       description = "Take snapper snapshot of root on boot";
       inherit documentation;
       serviceConfig.ExecStart = "${pkgs.snapper}/bin/snapper --config root create --cleanup-algorithm number --description boot";
-      serviceConfig.type = "oneshot";
+      serviceConfig.Type = "oneshot";
       requires = [ "local-fs.target" ];
       wantedBy = [ "multi-user.target" ];
       unitConfig.ConditionPathExists = "/etc/snapper/configs/root";
     };
 
+    assertions =
+      concatMap
+        (name:
+          let
+            sub = cfg.configs.${name};
+          in
+          [ { assertion = !(sub ? extraConfig);
+              message = ''
+                The option definition `services.snapper.configs.${name}.extraConfig' no longer has any effect; please remove it.
+                The contents of this option should be migrated to attributes on `services.snapper.configs.${name}'.
+              '';
+            }
+          ] ++
+          map
+            (attr: {
+              assertion = !(hasAttr attr sub);
+              message = ''
+                The option definition `services.snapper.configs.${name}.${attr}' has been renamed to `services.snapper.configs.${name}.${toUpper attr}'.
+              '';
+            })
+            [ "fstype" "subvolume" ]
+        )
+        (attrNames cfg.configs);
   });
 }
diff --git a/nixos/modules/services/misc/sourcehut/default.md b/nixos/modules/services/misc/sourcehut/default.md
new file mode 100644
index 00000000000..44d58aa0bef
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/default.md
@@ -0,0 +1,93 @@
+# Sourcehut {#module-services-sourcehut}
+
+[Sourcehut](https://sr.ht.com/) is an open-source,
+self-hostable software development platform. The server setup can be automated using
+[services.sourcehut](#opt-services.sourcehut.enable).
+
+## Basic usage {#module-services-sourcehut-basic-usage}
+
+Sourcehut is a Python and Go based set of applications.
+This NixOS module also provides basic configuration integrating Sourcehut into locally running
+`services.nginx`, `services.redis.servers.sourcehut`, `services.postfix`
+and `services.postgresql` services.
+
+A very basic configuration may look like this:
+```
+{ pkgs, ... }:
+let
+  fqdn =
+    let
+      join = hostName: domain: hostName + optionalString (domain != null) ".${domain}";
+    in join config.networking.hostName config.networking.domain;
+in {
+
+  networking = {
+    hostName = "srht";
+    domain = "tld";
+    firewall.allowedTCPPorts = [ 22 80 443 ];
+  };
+
+  services.sourcehut = {
+    enable = true;
+    git.enable = true;
+    man.enable = true;
+    meta.enable = true;
+    nginx.enable = true;
+    postfix.enable = true;
+    postgresql.enable = true;
+    redis.enable = true;
+    settings = {
+        "sr.ht" = {
+          environment = "production";
+          global-domain = fqdn;
+          origin = "https://${fqdn}";
+          # Produce keys with srht-keygen from sourcehut.coresrht.
+          network-key = "/run/keys/path/to/network-key";
+          service-key = "/run/keys/path/to/service-key";
+        };
+        webhooks.private-key= "/run/keys/path/to/webhook-key";
+    };
+  };
+
+  security.acme.certs."${fqdn}".extraDomainNames = [
+    "meta.${fqdn}"
+    "man.${fqdn}"
+    "git.${fqdn}"
+  ];
+
+  services.nginx = {
+    enable = true;
+    # only recommendedProxySettings are strictly required, but the rest make sense as well.
+    recommendedTlsSettings = true;
+    recommendedOptimisation = true;
+    recommendedGzipSettings = true;
+    recommendedProxySettings = true;
+
+    # Settings to setup what certificates are used for which endpoint.
+    virtualHosts = {
+      "${fqdn}".enableACME = true;
+      "meta.${fqdn}".useACMEHost = fqdn:
+      "man.${fqdn}".useACMEHost = fqdn:
+      "git.${fqdn}".useACMEHost = fqdn:
+    };
+  };
+}
+```
+
+  The `hostName` option is used internally to configure the nginx
+reverse-proxy. The `settings` attribute set is
+used by the configuration generator and the result is placed in `/etc/sr.ht/config.ini`.
+
+## Configuration {#module-services-sourcehut-configuration}
+
+All configuration parameters are also stored in
+`/etc/sr.ht/config.ini` which is generated by
+the module and linked from the store to ensure that all values from `config.ini`
+can be modified by the module.
+
+## Using an alternative webserver as reverse-proxy (e.g. `httpd`) {#module-services-sourcehut-httpd}
+
+By default, `nginx` is used as reverse-proxy for `sourcehut`.
+However, it's possible to use e.g. `httpd` by explicitly disabling
+`nginx` using [](#opt-services.nginx.enable) and fixing the
+`settings`.
diff --git a/nixos/modules/services/misc/sourcehut/default.nix b/nixos/modules/services/misc/sourcehut/default.nix
index 7dd254e3492..d4391bc49e3 100644
--- a/nixos/modules/services/misc/sourcehut/default.nix
+++ b/nixos/modules/services/misc/sourcehut/default.nix
@@ -438,7 +438,7 @@ in
         };
 
         options."lists.sr.ht" = commonServiceSettings "lists" // {
-          allow-new-lists = mkEnableOption (lib.mdDoc "Allow creation of new lists.");
+          allow-new-lists = mkEnableOption (lib.mdDoc "Allow creation of new lists");
           notify-from = mkOption {
             description = lib.mdDoc "Outgoing email for notifications generated by users.";
             type = types.str;
@@ -1390,6 +1390,6 @@ in
     '')
   ];
 
-  meta.doc = ./sourcehut.xml;
+  meta.doc = ./default.md;
   meta.maintainers = with maintainers; [ tomberek ];
 }
diff --git a/nixos/modules/services/misc/sourcehut/sourcehut.xml b/nixos/modules/services/misc/sourcehut/sourcehut.xml
deleted file mode 100644
index 41094f65a94..00000000000
--- a/nixos/modules/services/misc/sourcehut/sourcehut.xml
+++ /dev/null
@@ -1,119 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-sourcehut">
- <title>Sourcehut</title>
- <para>
-  <link xlink:href="https://sr.ht.com/">Sourcehut</link> is an open-source,
-  self-hostable software development platform. The server setup can be automated using
-  <link linkend="opt-services.sourcehut.enable">services.sourcehut</link>.
- </para>
-
- <section xml:id="module-services-sourcehut-basic-usage">
-  <title>Basic usage</title>
-  <para>
-   Sourcehut is a Python and Go based set of applications.
-   This NixOS module also provides basic configuration integrating Sourcehut into locally running
-   <literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>,
-   <literal><link linkend="opt-services.redis.servers">services.redis.servers.sourcehut</link></literal>,
-   <literal><link linkend="opt-services.postfix.enable">services.postfix</link></literal>
-   and
-   <literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal> services.
-  </para>
-
-  <para>
-   A very basic configuration may look like this:
-<programlisting>
-{ pkgs, ... }:
-let
-  fqdn =
-    let
-      join = hostName: domain: hostName + optionalString (domain != null) ".${domain}";
-    in join config.networking.hostName config.networking.domain;
-in {
-
-  networking = {
-    <link linkend="opt-networking.hostName">hostName</link> = "srht";
-    <link linkend="opt-networking.domain">domain</link> = "tld";
-    <link linkend="opt-networking.firewall.allowedTCPPorts">firewall.allowedTCPPorts</link> = [ 22 80 443 ];
-  };
-
-  services.sourcehut = {
-    <link linkend="opt-services.sourcehut.enable">enable</link> = true;
-    <link linkend="opt-services.sourcehut.git.enable">git.enable</link> = true;
-    <link linkend="opt-services.sourcehut.man.enable">man.enable</link> = true;
-    <link linkend="opt-services.sourcehut.meta.enable">meta.enable</link> = true;
-    <link linkend="opt-services.sourcehut.nginx.enable">nginx.enable</link> = true;
-    <link linkend="opt-services.sourcehut.postfix.enable">postfix.enable</link> = true;
-    <link linkend="opt-services.sourcehut.postgresql.enable">postgresql.enable</link> = true;
-    <link linkend="opt-services.sourcehut.redis.enable">redis.enable</link> = true;
-    <link linkend="opt-services.sourcehut.settings">settings</link> = {
-        "sr.ht" = {
-          environment = "production";
-          global-domain = fqdn;
-          origin = "https://${fqdn}";
-          # Produce keys with srht-keygen from <package>sourcehut.coresrht</package>.
-          network-key = "/run/keys/path/to/network-key";
-          service-key = "/run/keys/path/to/service-key";
-        };
-        webhooks.private-key= "/run/keys/path/to/webhook-key";
-    };
-  };
-
-  <link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."${fqdn}".extraDomainNames</link> = [
-    "meta.${fqdn}"
-    "man.${fqdn}"
-    "git.${fqdn}"
-  ];
-
-  services.nginx = {
-    <link linkend="opt-services.nginx.enable">enable</link> = true;
-    # only recommendedProxySettings are strictly required, but the rest make sense as well.
-    <link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true;
-    <link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true;
-    <link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true;
-    <link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true;
-
-    # Settings to setup what certificates are used for which endpoint.
-    <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
-      <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">"${fqdn}".enableACME</link> = true;
-      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"meta.${fqdn}".useACMEHost</link> = fqdn:
-      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"man.${fqdn}".useACMEHost</link> = fqdn:
-      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"git.${fqdn}".useACMEHost</link> = fqdn:
-    };
-  };
-}
-</programlisting>
-  </para>
-
-  <para>
-   The <literal>hostName</literal> option is used internally to configure the nginx
-   reverse-proxy. The <literal>settings</literal> attribute set is
-   used by the configuration generator and the result is placed in <literal>/etc/sr.ht/config.ini</literal>.
-  </para>
- </section>
-
- <section xml:id="module-services-sourcehut-configuration">
-  <title>Configuration</title>
-
-  <para>
-   All configuration parameters are also stored in
-   <literal>/etc/sr.ht/config.ini</literal> which is generated by
-   the module and linked from the store to ensure that all values from <literal>config.ini</literal>
-   can be modified by the module.
-  </para>
-
- </section>
-
- <section xml:id="module-services-sourcehut-httpd">
-  <title>Using an alternative webserver as reverse-proxy (e.g. <literal>httpd</literal>)</title>
-  <para>
-   By default, <package>nginx</package> is used as reverse-proxy for <package>sourcehut</package>.
-   However, it's possible to use e.g. <package>httpd</package> by explicitly disabling
-   <package>nginx</package> using <xref linkend="opt-services.nginx.enable" /> and fixing the
-   <literal>settings</literal>.
-  </para>
-</section>
-
-</chapter>
diff --git a/nixos/modules/services/misc/sssd.nix b/nixos/modules/services/misc/sssd.nix
index edd5750a4a4..7c7a3b464a8 100644
--- a/nixos/modules/services/misc/sssd.nix
+++ b/nixos/modules/services/misc/sssd.nix
@@ -77,6 +77,10 @@ in {
   };
   config = mkMerge [
     (mkIf cfg.enable {
+      # For `sssctl` to work.
+      environment.etc."sssd/sssd.conf".source = settingsFile;
+      environment.etc."sssd/conf.d".source = "${dataDir}/conf.d";
+
       systemd.services.sssd = {
         description = "System Security Services Daemon";
         wantedBy    = [ "multi-user.target" ];
@@ -101,6 +105,7 @@ in {
           EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
         };
         preStart = ''
+          mkdir -p "${dataDir}/conf.d"
           [ -f ${settingsFile} ] && rm -f ${settingsFile}
           old_umask=$(umask)
           umask 0177
diff --git a/nixos/modules/services/misc/tandoor-recipes.nix b/nixos/modules/services/misc/tandoor-recipes.nix
index a349bcac932..63d3e3d2a85 100644
--- a/nixos/modules/services/misc/tandoor-recipes.nix
+++ b/nixos/modules/services/misc/tandoor-recipes.nix
@@ -9,6 +9,7 @@ let
   env = {
     GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
     DEBUG = "0";
+    DEBUG_TOOLBAR = "0";
     MEDIA_ROOT = "/var/lib/tandoor-recipes";
   } // optionalAttrs (config.time.timeZone != null) {
     TIMEZONE = config.time.timeZone;
diff --git a/nixos/modules/services/misc/taskserver/default.md b/nixos/modules/services/misc/taskserver/default.md
new file mode 100644
index 00000000000..ee3b3908e2a
--- /dev/null
+++ b/nixos/modules/services/misc/taskserver/default.md
@@ -0,0 +1,93 @@
+# Taskserver {#module-services-taskserver}
+
+Taskserver is the server component of
+[Taskwarrior](https://taskwarrior.org/), a free and
+open source todo list application.
+
+*Upstream documentation:* <https://taskwarrior.org/docs/#taskd>
+
+## Configuration {#module-services-taskserver-configuration}
+
+Taskserver does all of its authentication via TLS using client certificates,
+so you either need to roll your own CA or purchase a certificate from a
+known CA, which allows creation of client certificates. These certificates
+are usually advertised as "server certificates".
+
+So in order to make it easier to handle your own CA, there is a helper tool
+called {command}`nixos-taskserver` which manages the custom CA along
+with Taskserver organisations, users and groups.
+
+While the client certificates in Taskserver only authenticate whether a user
+is allowed to connect, every user has its own UUID which identifies it as an
+entity.
+
+With {command}`nixos-taskserver` the client certificate is created
+along with the UUID of the user, so it handles all of the credentials needed
+in order to setup the Taskwarrior client to work with a Taskserver.
+
+## The nixos-taskserver tool {#module-services-taskserver-nixos-taskserver-tool}
+
+Because Taskserver by default only provides scripts to setup users
+imperatively, the {command}`nixos-taskserver` tool is used for
+addition and deletion of organisations along with users and groups defined
+by [](#opt-services.taskserver.organisations) and as well for
+imperative set up.
+
+The tool is designed to not interfere if the command is used to manually set
+up some organisations, users or groups.
+
+For example if you add a new organisation using {command}`nixos-taskserver
+org add foo`, the organisation is not modified and deleted no
+matter what you define in
+{option}`services.taskserver.organisations`, even if you're adding
+the same organisation in that option.
+
+The tool is modelled to imitate the official {command}`taskd`
+command, documentation for each subcommand can be shown by using the
+{option}`--help` switch.
+
+## Declarative/automatic CA management {#module-services-taskserver-declarative-ca-management}
+
+Everything is done according to what you specify in the module options,
+however in order to set up a Taskwarrior client for synchronisation with a
+Taskserver instance, you have to transfer the keys and certificates to the
+client machine.
+
+This is done using {command}`nixos-taskserver user export $orgname
+$username` which is printing a shell script fragment to stdout
+which can either be used verbatim or adjusted to import the user on the
+client machine.
+
+For example, let's say you have the following configuration:
+```ShellSession
+{
+  services.taskserver.enable = true;
+  services.taskserver.fqdn = "server";
+  services.taskserver.listenHost = "::";
+  services.taskserver.organisations.my-company.users = [ "alice" ];
+}
+```
+This creates an organisation called `my-company` with the
+user `alice`.
+
+Now in order to import the `alice` user to another machine
+`alicebox`, all we need to do is something like this:
+```ShellSession
+$ ssh server nixos-taskserver user export my-company alice | sh
+```
+Of course, if no SSH daemon is available on the server you can also copy
+&amp; paste it directly into a shell.
+
+After this step the user should be set up and you can start synchronising
+your tasks for the first time with {command}`task sync init` on
+`alicebox`.
+
+Subsequent synchronisation requests merely require the command {command}`task
+sync` after that stage.
+
+## Manual CA management {#module-services-taskserver-manual-ca-management}
+
+If you set any options within
+[service.taskserver.pki.manual](#opt-services.taskserver.pki.manual.ca.cert).*,
+{command}`nixos-taskserver` won't issue certificates, but you can
+still use it for adding or removing user accounts.
diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix
index ee4bf42183f..775b3b6d2ea 100644
--- a/nixos/modules/services/misc/taskserver/default.nix
+++ b/nixos/modules/services/misc/taskserver/default.nix
@@ -566,5 +566,5 @@ in {
     })
   ];
 
-  meta.doc = ./doc.xml;
+  meta.doc = ./default.md;
 }
diff --git a/nixos/modules/services/misc/taskserver/doc.xml b/nixos/modules/services/misc/taskserver/doc.xml
deleted file mode 100644
index f6ead7c3785..00000000000
--- a/nixos/modules/services/misc/taskserver/doc.xml
+++ /dev/null
@@ -1,135 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-    xmlns:xlink="http://www.w3.org/1999/xlink"
-    version="5.0"
-    xml:id="module-services-taskserver">
- <title>Taskserver</title>
- <para>
-  Taskserver is the server component of
-  <link xlink:href="https://taskwarrior.org/">Taskwarrior</link>, a free and
-  open source todo list application.
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="https://taskwarrior.org/docs/#taskd"/>
- </para>
- <section xml:id="module-services-taskserver-configuration">
-  <title>Configuration</title>
-
-  <para>
-   Taskserver does all of its authentication via TLS using client certificates,
-   so you either need to roll your own CA or purchase a certificate from a
-   known CA, which allows creation of client certificates. These certificates
-   are usually advertised as <quote>server certificates</quote>.
-  </para>
-
-  <para>
-   So in order to make it easier to handle your own CA, there is a helper tool
-   called <command>nixos-taskserver</command> which manages the custom CA along
-   with Taskserver organisations, users and groups.
-  </para>
-
-  <para>
-   While the client certificates in Taskserver only authenticate whether a user
-   is allowed to connect, every user has its own UUID which identifies it as an
-   entity.
-  </para>
-
-  <para>
-   With <command>nixos-taskserver</command> the client certificate is created
-   along with the UUID of the user, so it handles all of the credentials needed
-   in order to setup the Taskwarrior client to work with a Taskserver.
-  </para>
- </section>
- <section xml:id="module-services-taskserver-nixos-taskserver-tool">
-  <title>The nixos-taskserver tool</title>
-
-  <para>
-   Because Taskserver by default only provides scripts to setup users
-   imperatively, the <command>nixos-taskserver</command> tool is used for
-   addition and deletion of organisations along with users and groups defined
-   by <xref linkend="opt-services.taskserver.organisations"/> and as well for
-   imperative set up.
-  </para>
-
-  <para>
-   The tool is designed to not interfere if the command is used to manually set
-   up some organisations, users or groups.
-  </para>
-
-  <para>
-   For example if you add a new organisation using <command>nixos-taskserver
-   org add foo</command>, the organisation is not modified and deleted no
-   matter what you define in
-   <option>services.taskserver.organisations</option>, even if you're adding
-   the same organisation in that option.
-  </para>
-
-  <para>
-   The tool is modelled to imitate the official <command>taskd</command>
-   command, documentation for each subcommand can be shown by using the
-   <option>--help</option> switch.
-  </para>
- </section>
- <section xml:id="module-services-taskserver-declarative-ca-management">
-  <title>Declarative/automatic CA management</title>
-
-  <para>
-   Everything is done according to what you specify in the module options,
-   however in order to set up a Taskwarrior client for synchronisation with a
-   Taskserver instance, you have to transfer the keys and certificates to the
-   client machine.
-  </para>
-
-  <para>
-   This is done using <command>nixos-taskserver user export $orgname
-   $username</command> which is printing a shell script fragment to stdout
-   which can either be used verbatim or adjusted to import the user on the
-   client machine.
-  </para>
-
-  <para>
-   For example, let's say you have the following configuration:
-<screen>
-{
-  <xref linkend="opt-services.taskserver.enable"/> = true;
-  <xref linkend="opt-services.taskserver.fqdn"/> = "server";
-  <xref linkend="opt-services.taskserver.listenHost"/> = "::";
-  <link linkend="opt-services.taskserver.organisations._name_.users">services.taskserver.organisations.my-company.users</link> = [ "alice" ];
-}
-</screen>
-   This creates an organisation called <literal>my-company</literal> with the
-   user <literal>alice</literal>.
-  </para>
-
-  <para>
-   Now in order to import the <literal>alice</literal> user to another machine
-   <literal>alicebox</literal>, all we need to do is something like this:
-<screen>
-<prompt>$ </prompt>ssh server nixos-taskserver user export my-company alice | sh
-</screen>
-   Of course, if no SSH daemon is available on the server you can also copy
-   &amp; paste it directly into a shell.
-  </para>
-
-  <para>
-   After this step the user should be set up and you can start synchronising
-   your tasks for the first time with <command>task sync init</command> on
-   <literal>alicebox</literal>.
-  </para>
-
-  <para>
-   Subsequent synchronisation requests merely require the command <command>task
-   sync</command> after that stage.
-  </para>
- </section>
- <section xml:id="module-services-taskserver-manual-ca-management">
-  <title>Manual CA management</title>
-
-  <para>
-   If you set any options within
-   <link linkend="opt-services.taskserver.pki.manual.ca.cert">service.taskserver.pki.manual</link>.*,
-   <command>nixos-taskserver</command> won't issue certificates, but you can
-   still use it for adding or removing user accounts.
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/misc/weechat.md b/nixos/modules/services/misc/weechat.md
new file mode 100644
index 00000000000..21f41be5b4a
--- /dev/null
+++ b/nixos/modules/services/misc/weechat.md
@@ -0,0 +1,46 @@
+# WeeChat {#module-services-weechat}
+
+[WeeChat](https://weechat.org/) is a fast and
+extensible IRC client.
+
+## Basic Usage {#module-services-weechat-basic-usage}
+
+By default, the module creates a
+[`systemd`](https://www.freedesktop.org/wiki/Software/systemd/)
+unit which runs the chat client in a detached
+[`screen`](https://www.gnu.org/software/screen/)
+session.
+
+This can be done by enabling the `weechat` service:
+```
+{ ... }:
+
+{
+  services.weechat.enable = true;
+}
+```
+
+The service is managed by a dedicated user named `weechat`
+in the state directory `/var/lib/weechat`.
+
+## Re-attaching to WeeChat {#module-services-weechat-reattach}
+
+WeeChat runs in a screen session owned by a dedicated user. To explicitly
+allow your another user to attach to this session, the
+`screenrc` needs to be tweaked by adding
+[multiuser](https://www.gnu.org/software/screen/manual/html_node/Multiuser.html#Multiuser)
+support:
+```
+{
+  programs.screen.screenrc = ''
+    multiuser on
+    acladd normal_user
+  '';
+}
+```
+Now, the session can be re-attached like this:
+```
+screen -x weechat/weechat-screen
+```
+
+*The session name can be changed using [services.weechat.sessionName.](options.html#opt-services.weechat.sessionName)*
diff --git a/nixos/modules/services/misc/weechat.nix b/nixos/modules/services/misc/weechat.nix
index 663a767a0c1..338493e3cd3 100644
--- a/nixos/modules/services/misc/weechat.nix
+++ b/nixos/modules/services/misc/weechat.nix
@@ -15,7 +15,7 @@ in
       default = "/var/lib/weechat";
     };
     sessionName = mkOption {
-      description = lib.mdDoc "Name of the `screen' session for weechat.";
+      description = lib.mdDoc "Name of the `screen` session for weechat.";
       default = "weechat-screen";
       type = types.str;
     };
@@ -59,5 +59,5 @@ in
       };
   };
 
-  meta.doc = ./weechat.xml;
+  meta.doc = ./weechat.md;
 }
diff --git a/nixos/modules/services/misc/weechat.xml b/nixos/modules/services/misc/weechat.xml
deleted file mode 100644
index 7255edfb9da..00000000000
--- a/nixos/modules/services/misc/weechat.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-weechat">
- <title>WeeChat</title>
- <para>
-  <link xlink:href="https://weechat.org/">WeeChat</link> is a fast and
-  extensible IRC client.
- </para>
- <section xml:id="module-services-weechat-basic-usage">
-  <title>Basic Usage</title>
-
-  <para>
-   By default, the module creates a
-   <literal><link xlink:href="https://www.freedesktop.org/wiki/Software/systemd/">systemd</link></literal>
-   unit which runs the chat client in a detached
-   <literal><link xlink:href="https://www.gnu.org/software/screen/">screen</link></literal>
-   session.
-  </para>
-
-  <para>
-   This can be done by enabling the <literal>weechat</literal> service:
-<programlisting>
-{ ... }:
-
-{
-  <link linkend="opt-services.weechat.enable">services.weechat.enable</link> = true;
-}
-</programlisting>
-  </para>
-
-  <para>
-   The service is managed by a dedicated user named <literal>weechat</literal>
-   in the state directory <literal>/var/lib/weechat</literal>.
-  </para>
- </section>
- <section xml:id="module-services-weechat-reattach">
-  <title>Re-attaching to WeeChat</title>
-
-  <para>
-   WeeChat runs in a screen session owned by a dedicated user. To explicitly
-   allow your another user to attach to this session, the
-   <literal>screenrc</literal> needs to be tweaked by adding
-   <link xlink:href="https://www.gnu.org/software/screen/manual/html_node/Multiuser.html#Multiuser">multiuser</link>
-   support:
-<programlisting>
-{
-  <link linkend="opt-programs.screen.screenrc">programs.screen.screenrc</link> = ''
-    multiuser on
-    acladd normal_user
-  '';
-}
-</programlisting>
-   Now, the session can be re-attached like this:
-<programlisting>
-screen -x weechat/weechat-screen
-</programlisting>
-  </para>
-
-  <para>
-   <emphasis>The session name can be changed using
-   <link linkend="opt-services.weechat.sessionName">services.weechat.sessionName.</link></emphasis>
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
index 109415a20ee..616a60a123e 100644
--- a/nixos/modules/services/misc/zoneminder.nix
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -283,7 +283,8 @@ in {
       phpfpm = lib.mkIf useNginx {
         pools.zoneminder = {
           inherit user group;
-          phpPackage = pkgs.php.withExtensions ({ enabled, all }: enabled ++ [ all.apcu ]);
+          phpPackage = pkgs.php.withExtensions (
+            { enabled, all }: enabled ++ [ all.apcu all.sysvsem ]);
           phpOptions = ''
             date.timezone = "${config.time.timeZone}"
           '';
@@ -326,6 +327,15 @@ in {
           fi
 
           ${zoneminder}/bin/zmupdate.pl -nointeractive
+          ${zoneminder}/bin/zmupdate.pl --nointeractive -f
+
+          # Update ZM's Nix store path in the configuration table. Do nothing if the config doesn't
+          # contain ZM's Nix store path.
+          ${config.services.mysql.package}/bin/mysql -u zoneminder zm << EOF
+            UPDATE Config
+              SET Value = REGEXP_REPLACE(Value, "^/nix/store/[^-/]+-zoneminder-[^/]+", "${pkgs.zoneminder}")
+              WHERE Name = "ZM_FONT_FILE_LOCATION";
+          EOF
         '';
         serviceConfig = {
           User = user;
@@ -341,7 +351,7 @@ in {
           CacheDirectory = dirs cacheDirs;
           RuntimeDirectory = dirName;
           ReadWriteDirectories = lib.mkIf useCustomDir [ cfg.storageDir ];
-          StateDirectory = dirs (if useCustomDir then [] else libDirs);
+          StateDirectory = dirs (lib.optional (!useCustomDir) libDirs);
           LogsDirectory = dirName;
           PrivateTmp = true;
           ProtectSystem = "strict";
diff --git a/nixos/modules/services/monitoring/apcupsd.nix b/nixos/modules/services/monitoring/apcupsd.nix
index d4216b44cdc..666479c78a8 100644
--- a/nixos/modules/services/monitoring/apcupsd.nix
+++ b/nixos/modules/services/monitoring/apcupsd.nix
@@ -62,6 +62,21 @@ let
 
   );
 
+  # Ensure the CLI uses our generated configFile
+  wrappedBinaries = pkgs.runCommandLocal "apcupsd-wrapped-binaries"
+    { nativeBuildInputs = [ pkgs.makeWrapper ]; }
+    ''
+      for p in "${lib.getBin pkgs.apcupsd}/bin/"*; do
+          bname=$(basename "$p")
+          makeWrapper "$p" "$out/bin/$bname" --add-flags "-f ${configFile}"
+      done
+    '';
+
+  apcupsdWrapped = pkgs.symlinkJoin {
+    name = "apcupsd-wrapped";
+    # Put wrappers first so they "win"
+    paths = [ wrappedBinaries pkgs.apcupsd ];
+  };
 in
 
 {
@@ -138,7 +153,7 @@ in
     } ];
 
     # Give users access to the "apcaccess" tool
-    environment.systemPackages = [ pkgs.apcupsd ];
+    environment.systemPackages = [ apcupsdWrapped ];
 
     # NOTE 1: apcupsd runs as root because it needs permission to run
     # "shutdown"
diff --git a/nixos/modules/services/monitoring/below.nix b/nixos/modules/services/monitoring/below.nix
new file mode 100644
index 00000000000..92ee3882cac
--- /dev/null
+++ b/nixos/modules/services/monitoring/below.nix
@@ -0,0 +1,106 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.below;
+  cfgContents = concatStringsSep "\n" (
+    mapAttrsToList (n: v: ''${n} = "${v}"'') (filterAttrs (_k: v: v != null) {
+      log_dir = cfg.dirs.log;
+      store_dir = cfg.dirs.store;
+      cgroup_filter_out = cfg.cgroupFilterOut;
+    })
+  );
+
+  mkDisableOption = n: mkOption {
+    type = types.bool;
+    default = true;
+    description = mdDoc "Whether to enable ${n}.";
+  };
+  optionalType = ty: x: mkOption (x // {
+    description = mdDoc x.description;
+    type = (types.nullOr ty);
+    default = null;
+  });
+  optionalPath = optionalType types.path;
+  optionalStr = optionalType types.str;
+  optionalInt = optionalType types.int;
+in {
+  options = {
+    services.below = {
+      enable = mkEnableOption (mdDoc "'below' resource monitor");
+
+      cgroupFilterOut = optionalStr {
+        description = "A regexp matching the full paths of cgroups whose data shouldn't be collected";
+        example = "user.slice.*";
+      };
+      collect = {
+        diskStats = mkDisableOption "dist_stat collection";
+        ioStats   = mkEnableOption (mdDoc "io.stat collection for cgroups");
+        exitStats = mkDisableOption "eBPF-based exitstats";
+      };
+      compression.enable = mkEnableOption (mdDoc "data compression");
+      retention = {
+        size = optionalInt {
+          description = ''
+            Size limit for below's data, in bytes. Data is deleted oldest-first, in 24h 'shards'.
+
+            ::: {.note}
+            The size limit may be exceeded by at most the size of the active shard, as:
+            - the active shard cannot be deleted;
+            - the size limit is only enforced when a new shard is created.
+            :::
+          '';
+        };
+        time = optionalInt {
+          description = ''
+            Retention time, in seconds.
+
+            ::: {.note}
+            As data is stored in 24 hour shards which are discarded as a whole,
+            only data expired by 24h (or more) is guaranteed to be discarded.
+            :::
+
+            ::: {.note}
+            If `retention.size` is set, data may be discarded earlier than the specified time.
+            :::
+          '';
+        };
+      };
+      dirs = {
+        log = optionalPath { description = "Where to store below's logs"; };
+        store = optionalPath {
+          description = "Where to store below's data";
+          example = "/var/lib/below";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.below ];
+    # /etc/below.conf is also refered to by the `below` CLI tool,
+    #  so this can't be a store-only file whose path is passed to the service
+    environment.etc."below/below.conf".text = cfgContents;
+
+    systemd = {
+      packages = [ pkgs.below ];
+      services.below = {
+        # Workaround for https://github.com/NixOS/nixpkgs/issues/81138
+        wantedBy = [ "multi-user.target" ];
+        restartTriggers = [ cfgContents ];
+
+        serviceConfig.ExecStart = [
+          ""
+          ("${lib.getExe pkgs.below} record " + (concatStringsSep " " (
+            optional (!cfg.collect.diskStats) "--disable-disk-stat" ++
+            optional   cfg.collect.ioStats    "--collect-io-stat"   ++
+            optional (!cfg.collect.exitStats) "--disable-exitstats" ++
+            optional   cfg.compression.enable "--compress"          ++
+
+            optional (cfg.retention.size != null) "--store-size-limit ${toString cfg.retention.size}" ++
+            optional (cfg.retention.time != null) "--retain-for-s ${toString cfg.retention.time}"
+          )))
+        ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/cadvisor.nix b/nixos/modules/services/monitoring/cadvisor.nix
index a8fba4e6e8c..68e6e8e40b3 100644
--- a/nixos/modules/services/monitoring/cadvisor.nix
+++ b/nixos/modules/services/monitoring/cadvisor.nix
@@ -123,7 +123,7 @@ in {
             ${escapeShellArgs cfg.extraOptions} \
             ${optionalString (cfg.storageDriver != null) ''
               -storage_driver "${cfg.storageDriver}" \
-              -storage_driver_user "${cfg.storageDriverHost}" \
+              -storage_driver_host "${cfg.storageDriverHost}" \
               -storage_driver_db "${cfg.storageDriverDb}" \
               -storage_driver_user "${cfg.storageDriverUser}" \
               -storage_driver_password "$(cat "${cfg.storageDriverPasswordFile}")" \
diff --git a/nixos/modules/services/monitoring/cockpit.nix b/nixos/modules/services/monitoring/cockpit.nix
new file mode 100644
index 00000000000..2947b4d8012
--- /dev/null
+++ b/nixos/modules/services/monitoring/cockpit.nix
@@ -0,0 +1,231 @@
+{ pkgs, config, lib, ... }:
+
+let
+  cfg = config.services.cockpit;
+  inherit (lib) types mkEnableOption mkOption mkIf mdDoc literalMD mkPackageOptionMD;
+  settingsFormat = pkgs.formats.ini {};
+in {
+  options = {
+    services.cockpit = {
+      enable = mkEnableOption (mdDoc "Cockpit");
+
+      package = mkPackageOptionMD pkgs "Cockpit" {
+        default = [ "cockpit" ];
+      };
+
+      settings = lib.mkOption {
+        type = settingsFormat.type;
+
+        default = {};
+
+        description = mdDoc ''
+          Settings for cockpit that will be saved in /etc/cockpit/cockpit.conf.
+
+          See the [documentation](https://cockpit-project.org/guide/latest/cockpit.conf.5.html), that is also available with `man cockpit.conf.5` for details.
+        '';
+      };
+
+      port = mkOption {
+        description = mdDoc "Port where cockpit will listen.";
+        type = types.port;
+        default = 9090;
+      };
+
+      openFirewall = mkOption {
+        description = mdDoc "Open port for cockpit.";
+        type = types.bool;
+        default = false;
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+
+    # expose cockpit-bridge system-wide
+    environment.systemPackages = [ cfg.package ];
+
+    # allow cockpit to find its plugins
+    environment.pathsToLink = [ "/share/cockpit" ];
+
+    # generate cockpit settings
+    environment.etc."cockpit/cockpit.conf".source = settingsFormat.generate "cockpit.conf" cfg.settings;
+
+    security.pam.services.cockpit = {};
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+    # units are in reverse sort order if you ls $out/lib/systemd/system
+    # all these units are basically verbatim translated from upstream
+
+    # Translation from $out/lib/systemd/system/systemd-cockpithttps.slice
+    systemd.slices.system-cockpithttps = {
+      description = "Resource limits for all cockpit-ws-https@.service instances";
+      sliceConfig = {
+        TasksMax = 200;
+        MemoryHigh = "75%";
+        MemoryMax = "90%";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-wsinstance-https@.socket
+    systemd.sockets."cockpit-wsinstance-https@" = {
+      unitConfig = {
+        Description = "Socket for Cockpit Web Service https instance %I";
+        BindsTo = [ "cockpit.service" "cockpit-wsinstance-https@%i.service" ];
+        # clean up the socket after the service exits, to prevent fd leak
+        # this also effectively prevents a DoS by starting arbitrarily many sockets, as
+        # the services are resource-limited by system-cockpithttps.slice
+        Documentation = "man:cockpit-ws(8)";
+      };
+      socketConfig = {
+        ListenStream = "/run/cockpit/wsinstance/https@%i.sock";
+        SocketUser = "root";
+        SocketMode = "0600";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-wsinstance-https@.service
+    systemd.services."cockpit-wsinstance-https@" = {
+      description = "Cockpit Web Service https instance %I";
+      bindsTo = [ "cockpit.service"];
+      path = [ cfg.package ];
+      documentation = [ "man:cockpit-ws(8)" ];
+      serviceConfig = {
+        Slice = "system-cockpithttps.slice";
+        ExecStart = "${cfg.package}/libexec/cockpit-ws --for-tls-proxy --port=0";
+        User = "root";
+        Group = "";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-wsinstance-http.socket
+    systemd.sockets.cockpit-wsinstance-http = {
+      unitConfig = {
+        Description = "Socket for Cockpit Web Service http instance";
+        BindsTo = "cockpit.service";
+        Documentation = "man:cockpit-ws(8)";
+      };
+      socketConfig = {
+        ListenStream = "/run/cockpit/wsinstance/http.sock";
+        SocketUser = "root";
+        SocketMode = "0600";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-wsinstance-https-factory.socket
+    systemd.sockets.cockpit-wsinstance-https-factory = {
+      unitConfig = {
+        Description = "Socket for Cockpit Web Service https instance factory";
+        BindsTo = "cockpit.service";
+        Documentation = "man:cockpit-ws(8)";
+      };
+      socketConfig = {
+        ListenStream = "/run/cockpit/wsinstance/https-factory.sock";
+        Accept = true;
+        SocketUser = "root";
+        SocketMode = "0600";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-wsinstance-https-factory@.service
+    systemd.services."cockpit-wsinstance-https-factory@" = {
+      description = "Cockpit Web Service https instance factory";
+      documentation = [ "man:cockpit-ws(8)" ];
+      path = [ cfg.package ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/libexec/cockpit-wsinstance-factory";
+        User = "root";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-wsinstance-http.service
+    systemd.services."cockpit-wsinstance-http" = {
+      description = "Cockpit Web Service http instance";
+      bindsTo = [ "cockpit.service" ];
+      path = [ cfg.package ];
+      documentation = [ "man:cockpit-ws(8)" ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/libexec/cockpit-ws --no-tls --port=0";
+        User = "root";
+        Group = "";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit.socket
+    systemd.sockets."cockpit" = {
+      unitConfig = {
+        Description = "Cockpit Web Service Socket";
+        Documentation = "man:cockpit-ws(8)";
+        Wants = "cockpit-motd.service";
+      };
+      socketConfig = {
+        ListenStream = cfg.port;
+        ExecStartPost = [
+          "-${cfg.package}/share/cockpit/motd/update-motd \"\" localhost"
+          "-${pkgs.coreutils}/bin/ln -snf active.motd /run/cockpit/motd"
+        ];
+        ExecStopPost = "-${pkgs.coreutils}/bin/ln -snf inactive.motd /run/cockpit/motd";
+      };
+      wantedBy = [ "sockets.target" ];
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit.service
+    systemd.services."cockpit" = {
+      description = "Cockpit Web Service";
+      documentation = [ "man:cockpit-ws(8)" ];
+      restartIfChanged = true;
+      path = with pkgs; [ coreutils cfg.package ];
+      requires = [ "cockpit.socket" "cockpit-wsinstance-http.socket" "cockpit-wsinstance-https-factory.socket" ];
+      after = [ "cockpit-wsinstance-http.socket" "cockpit-wsinstance-https-factory.socket" ];
+      environment = {
+        G_MESSAGES_DEBUG = "cockpit-ws,cockpit-bridge";
+      };
+      serviceConfig = {
+        RuntimeDirectory="cockpit/tls";
+        ExecStartPre = [
+          # cockpit-tls runs in a more constrained environment, these + means that these commands
+          # will run with full privilege instead of inside that constrained environment
+          # See https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart= for details
+          "+${cfg.package}/libexec/cockpit-certificate-ensure --for-cockpit-tls"
+        ];
+        ExecStart = "${cfg.package}/libexec/cockpit-tls";
+        User = "root";
+        Group = "";
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        MemoryDenyWriteExecute = true;
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-motd.service
+    # This part basically implements a motd state machine:
+    # - If cockpit.socket is enabled then /run/cockpit/motd points to /run/cockpit/active.motd
+    # - If cockpit.socket is disabled then /run/cockpit/motd points to /run/cockpit/inactive.motd
+    # - As cockpit.socket is disabled by default, /run/cockpit/motd points to /run/cockpit/inactive.motd
+    # /run/cockpit/active.motd is generated dynamically by cockpit-motd.service
+    systemd.services."cockpit-motd" = {
+      path = with pkgs; [ nettools ];
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${cfg.package}/share/cockpit/motd/update-motd";
+      };
+      description = "Cockpit motd updater service";
+      documentation = [ "man:cockpit-ws(8)" ];
+      wants = [ "network.target" ];
+      after = [ "network.target" "cockpit.socket" ];
+    };
+
+    systemd.tmpfiles.rules = [ # From $out/lib/tmpfiles.d/cockpit-tmpfiles.conf
+      "C /run/cockpit/inactive.motd 0640 root root - ${cfg.package}/share/cockpit/motd/inactive.motd"
+      "f /run/cockpit/active.motd   0640 root root -"
+      "L+ /run/cockpit/motd - - - - inactive.motd"
+      "d /etc/cockpit/ws-certs.d 0600 root root 0"
+    ];
+  };
+
+  meta.maintainers = pkgs.cockpit.meta.maintainers;
+}
diff --git a/nixos/modules/services/monitoring/datadog-agent.nix b/nixos/modules/services/monitoring/datadog-agent.nix
index 15deef18b60..58a0faed962 100644
--- a/nixos/modules/services/monitoring/datadog-agent.nix
+++ b/nixos/modules/services/monitoring/datadog-agent.nix
@@ -235,7 +235,7 @@ in {
 
     systemd.services = let
       makeService = attrs: recursiveUpdate {
-        path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps pkgs.iproute2 ];
+        path = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute2 ];
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
           User = "datadog";
diff --git a/nixos/modules/services/monitoring/grafana-agent.nix b/nixos/modules/services/monitoring/grafana-agent.nix
index 270d888afb7..13604ff77c6 100644
--- a/nixos/modules/services/monitoring/grafana-agent.nix
+++ b/nixos/modules/services/monitoring/grafana-agent.nix
@@ -13,12 +13,7 @@ in
   options.services.grafana-agent = {
     enable = mkEnableOption (lib.mdDoc "grafana-agent");
 
-    package = mkOption {
-      type = types.package;
-      default = pkgs.grafana-agent;
-      defaultText = lib.literalExpression "pkgs.grafana-agent";
-      description = lib.mdDoc "The grafana-agent package to use.";
-    };
+    package = mkPackageOptionMD pkgs "grafana-agent" { };
 
     credentials = mkOption {
       description = lib.mdDoc ''
@@ -37,11 +32,22 @@ in
       };
     };
 
+    extraFlags = mkOption {
+      type = with types; listOf str;
+      default = [ ];
+      example = [ "-enable-features=integrations-next" "-disable-reporting" ];
+      description = lib.mdDoc ''
+        Extra command-line flags passed to {command}`grafana-agent`.
+
+        See <https://grafana.com/docs/agent/latest/static/configuration/flags/>
+      '';
+    };
+
     settings = mkOption {
       description = lib.mdDoc ''
-        Configuration for `grafana-agent`.
+        Configuration for {command}`grafana-agent`.
 
-        See https://grafana.com/docs/agent/latest/configuration/
+        See <https://grafana.com/docs/agent/latest/configuration/>
       '';
 
       type = types.submodule {
@@ -59,7 +65,6 @@ in
             agent.enabled = true;
             agent.scrape_integration = true;
             node_exporter.enabled = true;
-            replace_instance_label = true;
           };
         }
       '';
@@ -116,7 +121,6 @@ in
         agent.enabled = mkDefault true;
         agent.scrape_integration = mkDefault true;
         node_exporter.enabled = mkDefault true;
-        replace_instance_label = mkDefault true;
       };
     };
 
@@ -140,7 +144,7 @@ in
         # We can't use Environment=HOSTNAME=%H, as it doesn't include the domain part.
         export HOSTNAME=$(< /proc/sys/kernel/hostname)
 
-        exec ${cfg.package}/bin/agent -config.expand-env -config.file ${configFile}
+        exec ${lib.getExe cfg.package} -config.expand-env -config.file ${configFile} ${escapeShellArgs cfg.extraFlags}
       '';
       serviceConfig = {
         Restart = "always";
diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix
index 60f6e84c63c..36258866646 100644
--- a/nixos/modules/services/monitoring/grafana-image-renderer.nix
+++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix
@@ -107,8 +107,8 @@ in {
     ];
 
     services.grafana.settings.rendering = mkIf cfg.provisionGrafana {
-      url = "http://localhost:${toString cfg.settings.service.port}/render";
-      callback_url = "http://localhost:${toString config.services.grafana.port}";
+      server_url = "http://localhost:${toString cfg.settings.service.port}/render";
+      callback_url = "http://localhost:${toString config.services.grafana.settings.server.http_port}";
     };
 
     services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix
index 9cce4c71d96..571b9a3aeeb 100644
--- a/nixos/modules/services/monitoring/grafana.nix
+++ b/nixos/modules/services/monitoring/grafana.nix
@@ -5,25 +5,44 @@ with lib;
 let
   cfg = config.services.grafana;
   opt = options.services.grafana;
-  provisioningSettingsFormat = pkgs.formats.yaml {};
+  provisioningSettingsFormat = pkgs.formats.yaml { };
   declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins);
   useMysql = cfg.settings.database.type == "mysql";
   usePostgresql = cfg.settings.database.type == "postgres";
 
-  settingsFormatIni = pkgs.formats.ini {};
+  # Prefer using the values from the default config file[0] directly. This way,
+  # people reading the NixOS manual can see them without cross-referencing the
+  # official documentation.
+  #
+  # However, if there is no default entry or if the setting is optional, use
+  # `null` as the default value. It will be turned into the empty string.
+  #
+  # If a setting is a list, always allow setting it as a plain string as well.
+  #
+  # [0]: https://github.com/grafana/grafana/blob/main/conf/defaults.ini
+  settingsFormatIni = pkgs.formats.ini {
+    listToValue = concatMapStringsSep " " (generators.mkValueStringDefault { });
+    mkKeyValue = generators.mkKeyValueDefault
+      {
+        mkValueString = v:
+          if v == null then ""
+          else generators.mkValueStringDefault { } v;
+      }
+      "=";
+  };
   configFile = settingsFormatIni.generate "config.ini" cfg.settings;
 
   mkProvisionCfg = name: attr: provisionCfg:
     if provisionCfg.path != null
-      then provisionCfg.path
+    then provisionCfg.path
     else
       provisioningSettingsFormat.generate "${name}.yaml"
         (if provisionCfg.settings != null
-          then provisionCfg.settings
-          else {
-            apiVersion = 1;
-            ${attr} = [];
-          });
+        then provisionCfg.settings
+        else {
+          apiVersion = 1;
+          ${attr} = [ ];
+        });
 
   datasourceFileOrDir = mkProvisionCfg "datasource" "datasources" cfg.provision.datasources;
   dashboardFileOrDir = mkProvisionCfg "dashboard" "providers" cfg.provision.dashboards;
@@ -35,9 +54,10 @@ let
 
   notifierFileOrDir = 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;
+  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;
   rulesFileOrDir = generateAlertingProvisioningYaml "rules";
   contactPointsFileOrDir = generateAlertingProvisioningYaml "contactPoints";
   policiesFileOrDir = generateAlertingProvisioningYaml "policies";
@@ -56,7 +76,7 @@ let
   provisionConfDir = pkgs.runCommand "grafana-provisioning" { nativeBuildInputs = [ pkgs.xorg.lndir ]; } ''
     mkdir -p $out/{datasources,dashboards,notifiers,alerting}
     ${ln { src = datasourceFileOrDir;    dir = "datasources"; filename = "datasource"; }}
-    ${ln { src = dashboardFileOrDir;     dir = "dashboards";  filename = "dashbaord"; }}
+    ${ln { src = dashboardFileOrDir;     dir = "dashboards";  filename = "dashboard"; }}
     ${ln { src = notifierFileOrDir;      dir = "notifiers";   filename = "notifier"; }}
     ${ln { src = rulesFileOrDir;         dir = "alerting";    filename = "rules"; }}
     ${ln { src = contactPointsFileOrDir; dir = "alerting";    filename = "contactPoints"; }}
@@ -92,17 +112,6 @@ let
   grafanaTypes.datasourceConfig = types.submodule {
     freeformType = provisioningSettingsFormat.type;
 
-    imports = [
-      (mkRemovedOptionModule [ "password" ] ''
-        `services.grafana.provision.datasources.settings.datasources.<name>.password` has been removed
-        in Grafana 9. Use `secureJsonData` instead.
-      '')
-      (mkRemovedOptionModule [ "basicAuthPassword" ] ''
-        `services.grafana.provision.datasources.settings.datasources.<name>.basicAuthPassword` has been removed
-        in Grafana 9. Use `secureJsonData` instead.
-      '')
-    ];
-
     options = {
       name = mkOption {
         type = types.str;
@@ -113,7 +122,7 @@ let
         description = lib.mdDoc "Datasource type. Required.";
       };
       access = mkOption {
-        type = types.enum ["proxy" "direct"];
+        type = types.enum [ "proxy" "direct" ];
         default = "proxy";
         description = lib.mdDoc "Access mode. proxy or direct (Server or Browser in the UI). Required.";
       };
@@ -132,6 +141,11 @@ let
         default = false;
         description = lib.mdDoc "Allow users to edit datasources from the UI.";
       };
+      jsonData = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = lib.mdDoc "Extra data for datasource plugins.";
+      };
       secureJsonData = mkOption {
         type = types.nullOr types.attrs;
         default = null;
@@ -176,7 +190,7 @@ let
         description = lib.mdDoc "Notifier name.";
       };
       type = mkOption {
-        type = types.enum ["dingding" "discord" "email" "googlechat" "hipchat" "kafka" "line" "teams" "opsgenie" "pagerduty" "prometheus-alertmanager" "pushover" "sensu" "sensugo" "slack" "telegram" "threema" "victorops" "webhook"];
+        type = types.enum [ "dingding" "discord" "email" "googlechat" "hipchat" "kafka" "line" "teams" "opsgenie" "pagerduty" "prometheus-alertmanager" "pushover" "sensu" "sensugo" "slack" "telegram" "threema" "victorops" "webhook" ];
         description = lib.mdDoc "Notifier type.";
       };
       uid = mkOption {
@@ -231,7 +245,8 @@ let
       };
     };
   };
-in {
+in
+{
   imports = [
     (mkRenamedOptionModule [ "services" "grafana" "protocol" ] [ "services" "grafana" "settings" "server" "protocol" ])
     (mkRenamedOptionModule [ "services" "grafana" "addr" ] [ "services" "grafana" "settings" "server" "http_addr" ])
@@ -360,7 +375,7 @@ in {
             protocol = mkOption {
               description = lib.mdDoc "Which protocol to listen.";
               default = "http";
-              type = types.enum ["http" "https" "h2" "socket"];
+              type = types.enum [ "http" "https" "h2" "socket" ];
             };
 
             http_addr = mkOption {
@@ -382,17 +397,60 @@ in {
             };
 
             domain = mkOption {
-              description = lib.mdDoc "The public facing domain name used to access grafana from a browser.";
+              description = lib.mdDoc ''
+                The public facing domain name used to access grafana from a browser.
+
+                This setting is only used in the default value of the `root_url` setting.
+                If you set the latter manually, this option does not have to be specified.
+              '';
               default = "localhost";
               type = types.str;
             };
 
+            enforce_domain = mkOption {
+              description = lib.mdDoc ''
+                Redirect to correct domain if the host header does not match the domain.
+                Prevents DNS rebinding attacks.
+              '';
+              default = false;
+              type = types.bool;
+            };
+
             root_url = mkOption {
-              description = lib.mdDoc "Full public facing url.";
+              description = lib.mdDoc ''
+                This is the full URL used to access Grafana from a web browser.
+                This is important if you use Google or GitHub OAuth authentication (for the callback URL to be correct).
+
+                This setting is also important if you have a reverse proxy in front of Grafana that exposes it through a subpath.
+                In that case add the subpath to the end of this URL setting.
+              '';
               default = "%(protocol)s://%(domain)s:%(http_port)s/";
               type = types.str;
             };
 
+            serve_from_sub_path = mkOption {
+              description = lib.mdDoc ''
+                Serve Grafana from subpath specified in the `root_url` setting.
+                By default it is set to `false` for compatibility reasons.
+
+                By enabling this setting and using a subpath in `root_url` above,
+                e.g. `root_url = "http://localhost:3000/grafana"`,
+                Grafana is accessible on `http://localhost:3000/grafana`.
+                If accessed without subpath, Grafana will redirect to an URL with the subpath.
+              '';
+              default = false;
+              type = types.bool;
+            };
+
+            router_logging = mkOption {
+              description = lib.mdDoc ''
+                Set to `true` for Grafana to log all HTTP requests (not just errors).
+                These are logged as Info level events to the Grafana log.
+              '';
+              default = false;
+              type = types.bool;
+            };
+
             static_root_path = mkOption {
               description = lib.mdDoc "Root path for static assets.";
               default = "${cfg.package}/share/grafana/public";
@@ -402,60 +460,119 @@ in {
 
             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.
+                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;
+              description = lib.mdDoc ''
+                Path to the certificate file (if `protocol` is set to `https` or `h2`).
+              '';
+              default = null;
+              type = types.nullOr types.str;
             };
 
             cert_key = mkOption {
-              description = lib.mdDoc "Cert key for ssl.";
-              default = "";
+              description = lib.mdDoc ''
+                Path to the certificate key file (if `protocol` is set to `https` or `h2`).
+              '';
+              default = null;
+              type = types.nullOr types.str;
+            };
+
+            socket_gid = mkOption {
+              description = lib.mdDoc ''
+                GID where the socket should be set when `protocol=socket`.
+                Make sure that the target group is in the group of Grafana process and that Grafana process is the file owner before you change this setting.
+                It is recommended to set the gid as http server user gid.
+                Not set when the value is -1.
+              '';
+              default = -1;
+              type = types.int;
+            };
+
+            socket_mode = mkOption {
+              description = lib.mdDoc ''
+                Mode where the socket should be set when `protocol=socket`.
+                Make sure that Grafana process is the file owner before you change this setting.
+              '';
+              # I assume this value is interpreted as octal literal by grafana.
+              # If this was an int, people following tutorials or porting their
+              # old config could stumble across nix not having octal literals.
+              default = "0660";
               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.";
+              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;
             };
+
+            cdn_url = mkOption {
+              description = lib.mdDoc ''
+                Specify a full HTTP URL address to the root of your Grafana CDN assets.
+                Grafana will add edition and version paths.
+
+                For example, given a cdn url like `https://cdn.myserver.com`
+                grafana will try to load a javascript file from `http://cdn.myserver.com/grafana-oss/7.4.0/public/build/app.<hash>.js`.
+              '';
+              default = null;
+              type = types.nullOr types.str;
+            };
+
+            read_timeout = mkOption {
+              description = lib.mdDoc ''
+                Sets the maximum time using a duration format (5s/5m/5ms)
+                before timing out read of an incoming request and closing idle connections.
+                0 means there is no timeout for reading the request.
+              '';
+              default = "0";
+              type = types.str;
+            };
           };
 
           database = {
             type = mkOption {
               description = lib.mdDoc "Database type.";
               default = "sqlite3";
-              type = types.enum ["mysql" "sqlite3" "postgres"];
+              type = types.enum [ "mysql" "sqlite3" "postgres" ];
             };
 
             host = mkOption {
-              description = lib.mdDoc "Database host.";
+              description = lib.mdDoc ''
+                Only applicable to MySQL or Postgres.
+                Includes IP or hostname and port or in case of Unix sockets the path to it.
+                For example, for MySQL running on the same host as Grafana: `host = "127.0.0.1:3306"`
+                or with Unix sockets: `host = "/var/run/mysqld/mysqld.sock"`
+              '';
               default = "127.0.0.1:3306";
               type = types.str;
             };
 
             name = mkOption {
-              description = lib.mdDoc "Database name.";
+              description = lib.mdDoc "The name of the Grafana database.";
               default = "grafana";
               type = types.str;
             };
 
             user = mkOption {
-              description = lib.mdDoc "Database user.";
+              description = lib.mdDoc "The database user (not applicable for `sqlite3`).";
               default = "root";
               type = types.str;
             };
 
             password = mkOption {
               description = lib.mdDoc ''
-                Database password. Please note that the contents of this option
+                The database user's password (not applicable for `sqlite3`).
+
+                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:
@@ -465,15 +582,144 @@ in {
               type = types.str;
             };
 
+            max_idle_conn = mkOption {
+              description = lib.mdDoc "The maximum number of connections in the idle connection pool.";
+              default = 2;
+              type = types.int;
+            };
+
+            max_open_conn = mkOption {
+              description = lib.mdDoc "The maximum number of open connections to the database.";
+              default = 0;
+              type = types.int;
+            };
+
+            conn_max_lifetime = mkOption {
+              description = lib.mdDoc ''
+                Sets the maximum amount of time a connection may be reused.
+                The default is 14400 (which means 14400 seconds or 4 hours).
+                For MySQL, this setting should be shorter than the `wait_timeout` variable.
+              '';
+              default = 14400;
+              type = types.int;
+            };
+
+            locking_attempt_timeout_sec = mkOption {
+              description = lib.mdDoc ''
+                For `mysql`, if the `migrationLocking` feature toggle is set,
+                specify the time (in seconds) to wait before failing to lock the database for the migrations.
+              '';
+              default = 0;
+              type = types.int;
+            };
+
+            log_queries = mkOption {
+              description = lib.mdDoc "Set to `true` to log the sql calls and execution times";
+              default = false;
+              type = types.bool;
+            };
+
+            ssl_mode = mkOption {
+              description = lib.mdDoc ''
+                For Postgres, use either `disable`, `require` or `verify-full`.
+                For MySQL, use either `true`, `false`, or `skip-verify`.
+              '';
+              default = "disable";
+              type = types.enum [ "disable" "require" "verify-full" "true" "false" "skip-verify" ];
+            };
+
+            isolation_level = mkOption {
+              description = lib.mdDoc ''
+                Only the MySQL driver supports isolation levels in Grafana.
+                In case the value is empty, the driver's default isolation level is applied.
+              '';
+              default = null;
+              type = types.nullOr (types.enum [ "READ-UNCOMMITTED" "READ-COMMITTED" "REPEATABLE-READ" "SERIALIZABLE" ]);
+            };
+
+            ca_cert_path = mkOption {
+              description = lib.mdDoc "The path to the CA certificate to use.";
+              default = null;
+              type = types.nullOr types.str;
+            };
+
+            client_key_path = mkOption {
+              description = lib.mdDoc "The path to the client key. Only if server requires client authentication.";
+              default = null;
+              type = types.nullOr types.str;
+            };
+
+            client_cert_path = mkOption {
+              description = lib.mdDoc "The path to the client cert. Only if server requires client authentication.";
+              default = null;
+              type = types.nullOr types.str;
+            };
+
+            server_cert_name = mkOption {
+              description = lib.mdDoc ''
+                The common name field of the certificate used by the `mysql` or `postgres` server.
+                Not necessary if `ssl_mode` is set to `skip-verify`.
+              '';
+              default = null;
+              type = types.nullOr types.str;
+            };
+
             path = mkOption {
-              description = lib.mdDoc "Only applicable to sqlite3 database. The file path where the database will be stored.";
+              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;
             };
+
+            cache_mode = mkOption {
+              description = lib.mdDoc ''
+                For `sqlite3` only.
+                [Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database.
+              '';
+              default = "private";
+              type = types.enum [ "private" "shared" ];
+            };
+
+            wal = mkOption {
+              description = lib.mdDoc ''
+                For `sqlite3` only.
+                Setting to enable/disable [Write-Ahead Logging](https://sqlite.org/wal.html).
+              '';
+              default = false;
+              type = types.bool;
+            };
+
+            query_retries = mkOption {
+              description = lib.mdDoc ''
+                This setting applies to `sqlite3` only and controls the number of times the system retries a query when the database is locked.
+              '';
+              default = 0;
+              type = types.int;
+            };
+
+            transaction_retries = mkOption {
+              description = lib.mdDoc ''
+                This setting applies to `sqlite3` only and controls the number of times the system retries a transaction when the database is locked.
+              '';
+              default = 5;
+              type = types.int;
+            };
+
+            # TODO Add "instrument_queries" option when upgrading to grafana 10.0
+            # instrument_queries = mkOption {
+            #   description = lib.mdDoc "Set to `true` to add metrics and tracing for database queries.";
+            #   default = false;
+            #   type = types.bool;
+            # };
           };
 
           security = {
+            disable_initial_admin_creation = mkOption {
+              description = lib.mdDoc "Disable creation of admin user on first start of Grafana.";
+              default = false;
+              type = types.bool;
+            };
+
             admin_user = mkOption {
               description = lib.mdDoc "Default admin username.";
               default = "admin";
@@ -492,6 +738,12 @@ in {
               type = types.str;
             };
 
+            admin_email = mkOption {
+              description = lib.mdDoc "The email of the default Grafana Admin, created on startup.";
+              default = "admin@localhost";
+              type = types.str;
+            };
+
             secret_key = mkOption {
               description = lib.mdDoc ''
                 Secret key used for signing. Please note that the contents of this option
@@ -503,6 +755,160 @@ in {
               default = "SW2YcwTIb9zpOOhoPsMm";
               type = types.str;
             };
+
+            disable_gravatar = mkOption {
+              description = lib.mdDoc "Set to `true` to disable the use of Gravatar for user profile images.";
+              default = false;
+              type = types.bool;
+            };
+
+            data_source_proxy_whitelist = mkOption {
+              description = lib.mdDoc ''
+                Define a whitelist of allowed IP addresses or domains, with ports,
+                to be used in data source URLs with the Grafana data source proxy.
+                Format: `ip_or_domain:port` separated by spaces.
+                PostgreSQL, MySQL, and MSSQL data sources do not use the proxy and are therefore unaffected by this setting.
+              '';
+              default = [ ];
+              type = types.oneOf [ types.str (types.listOf types.str) ];
+            };
+
+            disable_brute_force_login_protection = mkOption {
+              description = lib.mdDoc "Set to `true` to disable [brute force login protection](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#account-lockout).";
+              default = false;
+              type = types.bool;
+            };
+
+            cookie_secure = mkOption {
+              description = lib.mdDoc "Set to `true` if you host Grafana behind HTTPS.";
+              default = false;
+              type = types.bool;
+            };
+
+            cookie_samesite = mkOption {
+              description = lib.mdDoc ''
+                Sets the `SameSite` cookie attribute and prevents the browser from sending this cookie along with cross-site requests.
+                The main goal is to mitigate the risk of cross-origin information leakage.
+                This setting also provides some protection against cross-site request forgery attacks (CSRF),
+                [read more about SameSite here](https://owasp.org/www-community/SameSite).
+                Using value `disabled` does not add any `SameSite` attribute to cookies.
+              '';
+              default = "lax";
+              type = types.enum [ "lax" "strict" "none" "disabled" ];
+            };
+
+            allow_embedding = mkOption {
+              description = lib.mdDoc ''
+                When `false`, the HTTP header `X-Frame-Options: deny` will be set in Grafana HTTP responses
+                which will instruct browsers to not allow rendering Grafana in a `<frame>`, `<iframe>`, `<embed>` or `<object>`.
+                The main goal is to mitigate the risk of [Clickjacking](https://owasp.org/www-community/attacks/Clickjacking).
+              '';
+              default = false;
+              type = types.bool;
+            };
+
+            strict_transport_security = mkOption {
+              description = lib.mdDoc ''
+                Set to `true` if you want to enable HTTP `Strict-Transport-Security` (HSTS) response header.
+                Only use this when HTTPS is enabled in your configuration,
+                or when there is another upstream system that ensures your application does HTTPS (like a frontend load balancer).
+                HSTS tells browsers that the site should only be accessed using HTTPS.
+              '';
+              default = false;
+              type = types.bool;
+            };
+
+            strict_transport_security_max_age_seconds = mkOption {
+              description = lib.mdDoc ''
+                Sets how long a browser should cache HSTS in seconds.
+                Only applied if `strict_transport_security` is enabled.
+              '';
+              default = 86400;
+              type = types.int;
+            };
+
+            strict_transport_security_preload = mkOption {
+              description = lib.mdDoc ''
+                Set to `true` to enable HSTS `preloading` option.
+                Only applied if `strict_transport_security` is enabled.
+              '';
+              default = false;
+              type = types.bool;
+            };
+
+            strict_transport_security_subdomains = mkOption {
+              description = lib.mdDoc ''
+                Set to `true` to enable HSTS `includeSubDomains` option.
+                Only applied if `strict_transport_security` is enabled.
+              '';
+              default = false;
+              type = types.bool;
+            };
+
+            x_content_type_options = mkOption {
+              description = lib.mdDoc ''
+                Set to `false` to disable the `X-Content-Type-Options` response header.
+                The `X-Content-Type-Options` response HTTP header is a marker used by the server
+                to indicate that the MIME types advertised in the `Content-Type` headers should not be changed and be followed.
+              '';
+              default = true;
+              type = types.bool;
+            };
+
+            x_xss_protection = mkOption {
+              description = lib.mdDoc ''
+                Set to `false` to disable the `X-XSS-Protection` header,
+                which tells browsers to stop pages from loading when they detect reflected cross-site scripting (XSS) attacks.
+              '';
+              default = true;
+              type = types.bool;
+            };
+
+            content_security_policy = mkOption {
+              description = lib.mdDoc ''
+                Set to `true` to add the `Content-Security-Policy` header to your requests.
+                CSP allows to control resources that the user agent can load and helps prevent XSS attacks.
+              '';
+              default = false;
+              type = types.bool;
+            };
+
+            content_security_policy_report_only = mkOption {
+              description = lib.mdDoc ''
+                Set to `true` to add the `Content-Security-Policy-Report-Only` header to your requests.
+                CSP in Report Only mode enables you to experiment with policies by monitoring their effects without enforcing them.
+                You can enable both policies simultaneously.
+              '';
+              default = false;
+              type = types.bool;
+            };
+
+            # The options content_security_policy_template and
+            # content_security_policy_template are missing because I'm not sure
+            # how exactly the quoting of the default value works. See also
+            # https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L364
+            # https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L373
+
+            # These two options are lists joined with spaces:
+            # https://github.com/grafana/grafana/blob/916d9793aa81c2990640b55a15dee0db6b525e41/pkg/middleware/csrf/csrf.go#L37-L38
+
+            csrf_trusted_origins = mkOption {
+              description = lib.mdDoc ''
+                List of additional allowed URLs to pass by the CSRF check.
+                Suggested when authentication comes from an IdP.
+              '';
+              default = [ ];
+              type = types.oneOf [ types.str (types.listOf types.str) ];
+            };
+
+            csrf_additional_headers = mkOption {
+              description = lib.mdDoc ''
+                List of allowed headers to be set by the user.
+                Suggested to use for if authentication lives behind reverse proxies.
+              '';
+              default = [ ];
+              type = types.oneOf [ types.str (types.listOf types.str) ];
+            };
           };
 
           smtp = {
@@ -511,16 +917,19 @@ in {
               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;
+              default = null;
+              type = types.nullOr types.str;
             };
+
             password = mkOption {
               description = lib.mdDoc ''
                 Password used for authentication. Please note that the contents of this option
@@ -532,43 +941,212 @@ in {
               default = "";
               type = types.str;
             };
+
+            cert_file = mkOption {
+              description = lib.mdDoc "File path to a cert file.";
+              default = null;
+              type = types.nullOr types.str;
+            };
+
+            key_file = mkOption {
+              description = lib.mdDoc "File path to a key file.";
+              default = null;
+              type = types.nullOr types.str;
+            };
+
+            skip_verify = mkOption {
+              description = lib.mdDoc "Verify SSL for SMTP server.";
+              default = false;
+              type = types.bool;
+            };
+
             from_address = mkOption {
-              description = lib.mdDoc "Email address used for sending.";
+              description = lib.mdDoc "Address used when sending out emails.";
               default = "admin@grafana.localhost";
               type = types.str;
             };
+
+            from_name = mkOption {
+              description = lib.mdDoc "Name to be used as client identity for EHLO in SMTP dialog.";
+              default = "Grafana";
+              type = types.str;
+            };
+
+            ehlo_identity = mkOption {
+              description = lib.mdDoc "Name to be used as client identity for EHLO in SMTP dialog.";
+              default = null;
+              type = types.nullOr types.str;
+            };
+
+            startTLS_policy = mkOption {
+              description = lib.mdDoc "StartTLS policy when connecting to server.";
+              default = null;
+              type = types.nullOr (types.enum [ "OpportunisticStartTLS" "MandatoryStartTLS" "NoStartTLS" ]);
+            };
           };
 
           users = {
             allow_sign_up = mkOption {
-              description = lib.mdDoc "Disable user signup / registration.";
+              description = lib.mdDoc ''
+                Set to false to prohibit users from being able to sign up / create user accounts.
+                The admin user can still create users.
+              '';
               default = false;
               type = types.bool;
             };
 
             allow_org_create = mkOption {
-              description = lib.mdDoc "Whether user is allowed to create organizations.";
+              description = lib.mdDoc "Set to `false` to prohibit users from creating new organizations.";
               default = false;
               type = types.bool;
             };
 
             auto_assign_org = mkOption {
-              description = lib.mdDoc "Whether to automatically assign new users to default org.";
+              description = lib.mdDoc ''
+                Set to `true` to automatically add new users to the main organization (id 1).
+                When set to `false,` new users automatically cause a new organization to be created for that new user.
+                The organization will be created even if the `allow_org_create` setting is set to `false`.
+              '';
               default = true;
               type = types.bool;
             };
 
+            auto_assign_org_id = mkOption {
+              description = lib.mdDoc ''
+                Set this value to automatically add new users to the provided org.
+                This requires `auto_assign_org` to be set to `true`.
+                Please make sure that this organization already exists.
+              '';
+              default = 1;
+              type = types.int;
+            };
+
             auto_assign_org_role = mkOption {
-              description = lib.mdDoc "Default role new users will be auto assigned.";
+              description = lib.mdDoc ''
+                The role new users will be assigned for the main organization (if the `auto_assign_org` setting is set to `true`).
+              '';
               default = "Viewer";
-              type = types.enum ["Viewer" "Editor" "Admin"];
+              type = types.enum [ "Viewer" "Editor" "Admin" ];
+            };
+
+            verify_email_enabled = mkOption {
+              description = lib.mdDoc "Require email validation before sign up completes.";
+              default = false;
+              type = types.bool;
+            };
+
+            login_hint = mkOption {
+              description = lib.mdDoc "Text used as placeholder text on login page for login/username input.";
+              default = "email or username";
+              type = types.str;
+            };
+
+            password_hint = mkOption {
+              description = lib.mdDoc "Text used as placeholder text on login page for password input.";
+              default = "password";
+              type = types.str;
+            };
+
+            default_theme = mkOption {
+              description = lib.mdDoc "Sets the default UI theme. `system` matches the user's system theme.";
+              default = "dark";
+              type = types.enum [ "dark" "light" "system" ];
+            };
+
+            default_language = mkOption {
+              description = lib.mdDoc "This setting configures the default UI language, which must be a supported IETF language tag, such as `en-US`.";
+              default = "en-US";
+              type = types.str;
+            };
+
+            home_page = mkOption {
+              description = lib.mdDoc ''
+                Path to a custom home page.
+                Users are only redirected to this if the default home dashboard is used.
+                It should match a frontend route and contain a leading slash.
+              '';
+              default = "";
+              type = types.str;
+            };
+
+            viewers_can_edit = mkOption {
+              description = lib.mdDoc ''
+                Viewers can access and use Explore and perform temporary edits on panels in dashboards they have access to.
+                They cannot save their changes.
+              '';
+              default = false;
+              type = types.bool;
+            };
+
+            editors_can_admin = mkOption {
+              description = lib.mdDoc "Editors can administrate dashboards, folders and teams they create.";
+              default = false;
+              type = types.bool;
+            };
+
+            user_invite_max_lifetime_duration = mkOption {
+              description = lib.mdDoc ''
+                The duration in time a user invitation remains valid before expiring.
+                This setting should be expressed as a duration.
+                Examples: `6h` (hours), `2d` (days), `1w` (week).
+                The minimum supported duration is `15m` (15 minutes).
+              '';
+              default = "24h";
+              type = types.str;
+            };
+
+            # Lists are joined via space, so this option can't be a list.
+            # Users have to manually join their values.
+            hidden_users = mkOption {
+              description = lib.mdDoc ''
+                This is a comma-separated list of usernames.
+                Users specified here are hidden in the Grafana UI.
+                They are still visible to Grafana administrators and to themselves.
+              '';
+              default = "";
+              type = types.str;
             };
           };
 
-          analytics.reporting_enabled = mkOption {
-            description = lib.mdDoc "Whether to allow anonymous usage reporting to stats.grafana.net.";
-            default = true;
-            type = types.bool;
+          analytics = {
+            reporting_enabled = mkOption {
+              description = lib.mdDoc ''
+                When enabled Grafana will send anonymous usage statistics to `stats.grafana.org`.
+                No IP addresses are being tracked, only simple counters to track running instances, versions, dashboard and error counts.
+                Counters are sent every 24 hours.
+              '';
+              default = true;
+              type = types.bool;
+            };
+
+            check_for_updates = mkOption {
+              description = lib.mdDoc ''
+                When set to `false`, disables checking for new versions of Grafana from Grafana's GitHub repository.
+                When enabled, the check for a new version runs every 10 minutes.
+                It will notify, via the UI, when a new version is available.
+                The check itself will not prompt any auto-updates of the Grafana software, nor will it send any sensitive information.
+              '';
+              default = false;
+              type = types.bool;
+            };
+
+            check_for_plugin_updates = mkOption {
+              description = lib.mdDoc ''
+                When set to `false`, disables checking for new versions of installed plugins from https://grafana.com.
+                When enabled, the check for a new plugin runs every 10 minutes.
+                It will notify, via the UI, when a new plugin update exists.
+                The check itself will not prompt any auto-updates of the plugin, nor will it send any sensitive information.
+              '';
+              default = cfg.declarativePlugins == null;
+              defaultText = literalExpression "cfg.declarativePlugins == null";
+              type = types.bool;
+            };
+
+            feedback_links_enabled = mkOption {
+              description = lib.mdDoc "Set to `false` to remove all feedback links from the UI.";
+              default = true;
+              type = types.bool;
+            };
           };
         };
       };
@@ -581,7 +1159,7 @@ in {
         description = lib.mdDoc ''
           Declaratively provision Grafana's datasources.
         '';
-        default = {};
+        default = { };
         type = submodule' {
           options.settings = mkOption {
             description = lib.mdDoc ''
@@ -601,14 +1179,13 @@ in {
 
                 datasources = mkOption {
                   description = lib.mdDoc "List of datasources to insert/update.";
-                  default = [];
+                  default = [ ];
                   type = types.listOf grafanaTypes.datasourceConfig;
-                  apply = map (flip builtins.removeAttrs [ "password" "basicAuthPassword" ]);
                 };
 
                 deleteDatasources = mkOption {
                   description = lib.mdDoc "List of datasources that should be deleted from the database.";
-                  default = [];
+                  default = [ ];
                   type = types.listOf (types.submodule {
                     options.name = mkOption {
                       description = lib.mdDoc "Name of the datasource to delete.";
@@ -657,7 +1234,7 @@ in {
         description = lib.mdDoc ''
           Declaratively provision Grafana's dashboards.
         '';
-        default = {};
+        default = { };
         type = submodule' {
           options.settings = mkOption {
             description = lib.mdDoc ''
@@ -676,7 +1253,7 @@ in {
 
               options.providers = mkOption {
                 description = lib.mdDoc "List of dashboards to insert/update.";
-                default = [];
+                default = [ ];
                 type = types.listOf grafanaTypes.dashboardConfig;
               };
             });
@@ -707,7 +1284,7 @@ in {
 
       notifiers = mkOption {
         description = lib.mdDoc "Grafana notifier configuration.";
-        default = [];
+        default = [ ];
         type = types.listOf grafanaTypes.notifierConfig;
         apply = x: map _filter x;
       };
@@ -743,7 +1320,7 @@ in {
 
                 groups = mkOption {
                   description = lib.mdDoc "List of rule groups to import or update.";
-                  default = [];
+                  default = [ ];
                   type = types.listOf (types.submodule {
                     freeformType = provisioningSettingsFormat.type;
 
@@ -766,7 +1343,7 @@ in {
 
                 deleteRules = mkOption {
                   description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
-                  default = [];
+                  default = [ ];
                   type = types.listOf (types.submodule {
                     options.orgId = mkOption {
                       description = lib.mdDoc "Organization ID, default = 1";
@@ -867,7 +1444,7 @@ in {
 
                 contactPoints = mkOption {
                   description = lib.mdDoc "List of contact points to import or update.";
-                  default = [];
+                  default = [ ];
                   type = types.listOf (types.submodule {
                     freeformType = provisioningSettingsFormat.type;
 
@@ -880,7 +1457,7 @@ in {
 
                 deleteContactPoints = mkOption {
                   description = lib.mdDoc "List of receivers that should be deleted.";
-                  default = [];
+                  default = [ ];
                   type = types.listOf (types.submodule {
                     options.orgId = mkOption {
                       description = lib.mdDoc "Organization ID, default = 1.";
@@ -948,7 +1525,7 @@ in {
 
                 policies = mkOption {
                   description = lib.mdDoc "List of contact points to import or update.";
-                  default = [];
+                  default = [ ];
                   type = types.listOf (types.submodule {
                     freeformType = provisioningSettingsFormat.type;
                   });
@@ -956,7 +1533,7 @@ in {
 
                 resetPolicies = mkOption {
                   description = lib.mdDoc "List of orgIds that should be reset to the default policy.";
-                  default = [];
+                  default = [ ];
                   type = types.listOf types.int;
                 };
               };
@@ -1018,7 +1595,7 @@ in {
 
                 templates = mkOption {
                   description = lib.mdDoc "List of templates to import or update.";
-                  default = [];
+                  default = [ ];
                   type = types.listOf (types.submodule {
                     freeformType = provisioningSettingsFormat.type;
 
@@ -1036,7 +1613,7 @@ in {
 
                 deleteTemplates = mkOption {
                   description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
-                  default = [];
+                  default = [ ];
                   type = types.listOf (types.submodule {
                     options.orgId = mkOption {
                       description = lib.mdDoc "Organization ID, default = 1.";
@@ -1100,7 +1677,7 @@ in {
 
                 muteTimes = mkOption {
                   description = lib.mdDoc "List of mute time intervals to import or update.";
-                  default = [];
+                  default = [ ];
                   type = types.listOf (types.submodule {
                     freeformType = provisioningSettingsFormat.type;
 
@@ -1113,7 +1690,7 @@ in {
 
                 deleteMuteTimes = mkOption {
                   description = lib.mdDoc "List of mute time intervals that should be deleted.";
-                  default = [];
+                  default = [ ];
                   type = types.listOf (types.submodule {
                     options.orgId = mkOption {
                       description = lib.mdDoc "Organization ID, default = 1.";
@@ -1175,45 +1752,58 @@ in {
   };
 
   config = mkIf cfg.enable {
-    warnings = let
-      doesntUseFileProvider = opt: defaultValue:
-        let
-          regex = "${optionalString (defaultValue != null) "^${defaultValue}$|"}^\\$__(file|env)\\{.*}$|^\\$[^_\\$][^ ]+$";
-        in builtins.match regex opt == null;
-    in
-      # Ensure that no custom credentials are leaked into the Nix store. Unless the default value
-      # is specified, this can be achieved by using the file/env provider:
-      # https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#variable-expansion
-      (optional (
-        doesntUseFileProvider cfg.settings.database.password "" ||
-        doesntUseFileProvider cfg.settings.security.admin_password "admin"
-      ) ''
-        Grafana passwords will be stored as plaintext in the Nix store!
-        Use file provider or an env-var instead.
-      '')
-      # Warn about deprecated notifiers.
-      ++ (optional (cfg.provision.notifiers != []) ''
-        Notifiers are deprecated upstream and will be removed in Grafana 10.
-        Use `services.grafana.provision.alerting.contactPoints` instead.
-      '')
-      # Ensure that `secureJsonData` of datasources provisioned via `datasources.settings`
-      # only uses file/env providers.
-      ++ (optional (
-        let
-          datasourcesToCheck = optionals
-            (cfg.provision.datasources.settings != null)
-            cfg.provision.datasources.settings.datasources;
-          declarationUnsafe = { secureJsonData, ... }:
-            secureJsonData != null
-            && any (flip doesntUseFileProvider null) (attrValues secureJsonData);
-        in any declarationUnsafe datasourcesToCheck
-      ) ''
-        Declarations in the `secureJsonData`-block of a datasource will be leaked to the
-        Nix store unless a file-provider or an env-var is used!
-      '')
-      ++ (optional (
-        any (x: x.secure_settings != null) cfg.provision.notifiers
-      ) "Notifier secure settings will be stored as plaintext in the Nix store! Use file provider instead.");
+    warnings =
+      let
+        doesntUseFileProvider = opt: defaultValue:
+          let regex = "${optionalString (defaultValue != null) "^${defaultValue}$|"}^\\$__(file|env)\\{.*}$|^\\$[^_\\$][^ ]+$";
+          in builtins.match regex opt == null;
+
+        # Ensure that no custom credentials are leaked into the Nix store. Unless the default value
+        # is specified, this can be achieved by using the file/env provider:
+        # https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#variable-expansion
+        passwordWithoutFileProvider = optional
+          (
+            doesntUseFileProvider cfg.settings.database.password "" ||
+            doesntUseFileProvider cfg.settings.security.admin_password "admin"
+          )
+          ''
+            Grafana passwords will be stored as plaintext in the Nix store!
+            Use file provider or an env-var instead.
+          '';
+
+        # Warn about deprecated notifiers.
+        deprecatedNotifiers = optional (cfg.provision.notifiers != [ ]) ''
+          Notifiers are deprecated upstream and will be removed in Grafana 11.
+          Use `services.grafana.provision.alerting.contactPoints` instead.
+        '';
+
+        # Ensure that `secureJsonData` of datasources provisioned via `datasources.settings`
+        # only uses file/env providers.
+        secureJsonDataWithoutFileProvider = optional
+          (
+            let
+              datasourcesToCheck = optionals
+                (cfg.provision.datasources.settings != null)
+                cfg.provision.datasources.settings.datasources;
+              declarationUnsafe = { secureJsonData, ... }:
+                secureJsonData != null
+                && any (flip doesntUseFileProvider null) (attrValues secureJsonData);
+            in
+            any declarationUnsafe datasourcesToCheck
+          )
+          ''
+            Declarations in the `secureJsonData`-block of a datasource will be leaked to the
+            Nix store unless a file-provider or an env-var is used!
+          '';
+
+        notifierSecureSettingsWithoutFileProvider = optional
+          (any (x: x.secure_settings != null) cfg.provision.notifiers)
+          "Notifier secure settings will be stored as plaintext in the Nix store! Use file provider instead.";
+      in
+      passwordWithoutFileProvider
+      ++ deprecatedNotifiers
+      ++ secureJsonDataWithoutFileProvider
+      ++ notifierSecureSettingsWithoutFileProvider;
 
     environment.systemPackages = [ cfg.package ];
 
@@ -1223,11 +1813,12 @@ in {
         message = "Cannot set both datasources settings and datasources path";
       }
       {
-        assertion = let
-          prometheusIsNotDirect = opt: all
-          ({ type, access, ... }: type == "prometheus" -> access != "direct")
-          opt;
-        in
+        assertion =
+          let
+            prometheusIsNotDirect = opt: all
+              ({ type, access, ... }: type == "prometheus" -> access != "direct")
+              opt;
+          in
           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)";
       }
@@ -1259,8 +1850,8 @@ in {
 
     systemd.services.grafana = {
       description = "Grafana Service Daemon";
-      wantedBy = ["multi-user.target"];
-      after = ["networking.target"] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
       script = ''
         set -o errexit -o pipefail -o nounset -o errtrace
         shopt -s inherit_errexit
@@ -1300,7 +1891,7 @@ in {
         SystemCallFilter = [
           "@system-service"
           "~@privileged"
-        ] ++ lib.optional (cfg.settings.server.protocol == "socket") [ "@chown" ];
+        ] ++ lib.optionals (cfg.settings.server.protocol == "socket") [ "@chown" ];
         UMask = "0027";
       };
       preStart = ''
@@ -1316,6 +1907,6 @@ in {
       createHome = true;
       group = "grafana";
     };
-    users.groups.grafana = {};
+    users.groups.grafana = { };
   };
 }
diff --git a/nixos/modules/services/monitoring/loki.nix b/nixos/modules/services/monitoring/loki.nix
index 11bb8497c9b..f3b97e9151e 100644
--- a/nixos/modules/services/monitoring/loki.nix
+++ b/nixos/modules/services/monitoring/loki.nix
@@ -22,6 +22,8 @@ in {
       '';
     };
 
+    package = lib.mkPackageOptionMD pkgs "grafana-loki" { };
+
     group = mkOption {
       type = types.str;
       default = "loki";
@@ -78,7 +80,7 @@ in {
       '';
     }];
 
-    environment.systemPackages = [ pkgs.grafana-loki ]; # logcli
+    environment.systemPackages = [ cfg.package ]; # logcli
 
     users.groups.${cfg.group} = { };
     users.users.${cfg.user} = {
@@ -99,7 +101,7 @@ in {
                else cfg.configFile;
       in
       {
-        ExecStart = "${pkgs.grafana-loki}/bin/loki --config.file=${conf} ${escapeShellArgs cfg.extraFlags}";
+        ExecStart = "${cfg.package}/bin/loki --config.file=${conf} ${escapeShellArgs cfg.extraFlags}";
         User = cfg.user;
         Restart = "always";
         PrivateTmp = true;
diff --git a/nixos/modules/services/monitoring/mackerel-agent.nix b/nixos/modules/services/monitoring/mackerel-agent.nix
index 4185cd76c4e..67dc1bc19ed 100644
--- a/nixos/modules/services/monitoring/mackerel-agent.nix
+++ b/nixos/modules/services/monitoring/mackerel-agent.nix
@@ -11,7 +11,7 @@ in {
 
     # the upstream package runs as root, but doesn't seem to be strictly
     # necessary for basic functionality
-    runAsRoot = mkEnableOption (lib.mdDoc "Whether to run as root.");
+    runAsRoot = mkEnableOption (lib.mdDoc "Whether to run as root");
 
     autoRetirement = mkEnableOption (lib.mdDoc ''
       Whether to automatically retire the host upon OS shutdown.
diff --git a/nixos/modules/services/monitoring/mimir.nix b/nixos/modules/services/monitoring/mimir.nix
index 568066990f2..edca9b7be4f 100644
--- a/nixos/modules/services/monitoring/mimir.nix
+++ b/nixos/modules/services/monitoring/mimir.nix
@@ -25,6 +25,13 @@ in {
         Specify a configuration file that Mimir should use.
       '';
     };
+
+    package = mkOption {
+      default = pkgs.mimir;
+      defaultText = lib.literalExpression "pkgs.mimir";
+      type = types.package;
+      description = lib.mdDoc ''Mimir package to use.'';
+    };
   };
 
   config = mkIf cfg.enable {
@@ -53,7 +60,7 @@ in {
                else cfg.configFile;
       in
       {
-        ExecStart = "${pkgs.mimir}/bin/mimir --config.file=${conf}";
+        ExecStart = "${cfg.package}/bin/mimir --config.file=${conf}";
         DynamicUser = true;
         Restart = "always";
         ProtectSystem = "full";
diff --git a/nixos/modules/services/monitoring/netdata.nix b/nixos/modules/services/monitoring/netdata.nix
index 92c870bb23f..ebfbbf8f37c 100644
--- a/nixos/modules/services/monitoring/netdata.nix
+++ b/nixos/modules/services/monitoring/netdata.nix
@@ -159,6 +159,15 @@ in {
         '';
       };
 
+      claimTokenFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = lib.mdDoc ''
+          If set, automatically registers the agent using the given claim token
+          file.
+        '';
+      };
+
       enableAnalyticsReporting = mkOption {
         type = types.bool;
         default = false;
@@ -169,6 +178,20 @@ in {
           See: <https://learn.netdata.cloud/docs/agent/anonymous-statistics>
         '';
       };
+
+      deadlineBeforeStopSec = mkOption {
+        type = types.int;
+        default = 120;
+        description = lib.mdDoc ''
+          In order to detect when netdata is misbehaving, we run a concurrent task pinging netdata (wait-for-netdata-up)
+          in the systemd unit.
+
+          If after a while, this task does not succeed, we stop the unit and mark it as failed.
+
+          You can control this deadline in seconds with this option, it's useful to bump it
+          if you have (1) a lot of data (2) doing upgrades (3) have low IOPS/throughput.
+        '';
+      };
     };
   };
 
@@ -205,7 +228,7 @@ in {
           while [ "$(${pkgs.netdata}/bin/netdatacli ping)" != pong ]; do sleep 0.5; done
         '';
 
-        TimeoutStopSec = 60;
+        TimeoutStopSec = cfg.deadlineBeforeStopSec;
         Restart = "on-failure";
         # User and group
         User = cfg.user;
@@ -246,7 +269,25 @@ in {
         PrivateTmp = true;
         ProtectControlGroups = true;
         PrivateMounts = true;
-      };
+      } // (lib.optionalAttrs (cfg.claimTokenFile != null) {
+        LoadCredential = [
+          "netdata_claim_token:${cfg.claimTokenFile}"
+        ];
+
+        ExecStartPre = pkgs.writeShellScript "netdata-claim" ''
+          set -euo pipefail
+
+          if [[ -f /var/lib/netdata/cloud.d/claimed_id ]]; then
+            # Already registered
+            exit
+          fi
+
+          exec ${cfg.package}/bin/netdata-claim.sh \
+            -token="$(< "$CREDENTIALS_DIRECTORY/netdata_claim_token")" \
+            -url=https://app.netdata.cloud \
+            -daemon-not-running
+        '';
+      });
     };
 
     systemd.enableCgroupAccounting = true;
diff --git a/nixos/modules/services/monitoring/opentelemetry-collector.nix b/nixos/modules/services/monitoring/opentelemetry-collector.nix
new file mode 100644
index 00000000000..1d211b68977
--- /dev/null
+++ b/nixos/modules/services/monitoring/opentelemetry-collector.nix
@@ -0,0 +1,73 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkEnableOption mkIf mkOption types getExe;
+
+  cfg = config.services.opentelemetry-collector;
+  opentelemetry-collector = cfg.package;
+
+  settingsFormat = pkgs.formats.yaml {};
+in {
+  options.services.opentelemetry-collector = {
+    enable = mkEnableOption (lib.mdDoc "Opentelemetry Collector");
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.opentelemetry-collector;
+      defaultText = lib.literalExpression "pkgs.opentelemetry-collector";
+      description = lib.mdDoc "The opentelemetry-collector package to use.";
+    };
+
+    settings = mkOption {
+      type = settingsFormat.type;
+      default = {};
+      description = lib.mdDoc ''
+        Specify the configuration for Opentelemetry Collector in Nix.
+
+        See https://opentelemetry.io/docs/collector/configuration/ for available options.
+      '';
+    };
+
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = lib.mdDoc ''
+        Specify a path to a configuration file that Opentelemetry Collector should use.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [{
+      assertion = (
+        (cfg.settings == {}) != (cfg.configFile == null)
+      );
+      message  = ''
+        Please specify a configuration for Opentelemetry Collector with either
+        'services.opentelemetry-collector.settings' or
+        'services.opentelemetry-collector.configFile'.
+      '';
+    }];
+
+    systemd.services.opentelemetry-collector = {
+      description = "Opentelemetry Collector Service Daemon";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = let
+        conf = if cfg.configFile == null
+               then settingsFormat.generate "config.yaml" cfg.settings
+               else cfg.configFile;
+      in
+      {
+        ExecStart = "${getExe opentelemetry-collector} --config=file:${conf}";
+        DynamicUser = true;
+        Restart = "always";
+        ProtectSystem = "full";
+        DevicePolicy = "closed";
+        NoNewPrivileges = true;
+        WorkingDirectory = "/var/lib/opentelemetry-collector";
+        StateDirectory = "opentelemetry-collector";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/parsedmarc.md b/nixos/modules/services/monitoring/parsedmarc.md
index 5a17f79da5d..eac07e0cc9f 100644
--- a/nixos/modules/services/monitoring/parsedmarc.md
+++ b/nixos/modules/services/monitoring/parsedmarc.md
@@ -25,7 +25,7 @@ services.parsedmarc = {
 Note that GeoIP provisioning is disabled in the example for
 simplicity, but should be turned on for fully functional reports.
 
-## Local mail
+## Local mail {#module-services-parsedmarc-local-mail}
 Instead of watching an external inbox, a local inbox can be
 automatically provisioned. The recipient's name is by default set to
 `dmarc`, but can be configured in
@@ -49,7 +49,7 @@ services.parsedmarc = {
 };
 ```
 
-## Grafana and GeoIP
+## Grafana and GeoIP {#module-services-parsedmarc-grafana-geoip}
 The reports can be visualized and summarized with parsedmarc's
 official Grafana dashboard. For all views to work, and for the data to
 be complete, GeoIP databases are also required. The following example
diff --git a/nixos/modules/services/monitoring/parsedmarc.nix b/nixos/modules/services/monitoring/parsedmarc.nix
index 40c76b80455..44fc359b6a7 100644
--- a/nixos/modules/services/monitoring/parsedmarc.nix
+++ b/nixos/modules/services/monitoring/parsedmarc.nix
@@ -409,7 +409,7 @@ in
 
       provision = {
         enable = cfg.provision.grafana.datasource || cfg.provision.grafana.dashboard;
-        datasources =
+        datasources.settings.datasources =
           let
             esVersion = lib.getVersion config.services.elasticsearch.package;
           in
@@ -435,7 +435,7 @@ in
                 };
               }
             ];
-        dashboards = lib.mkIf cfg.provision.grafana.dashboard [{
+        dashboards.settings.providers = lib.mkIf cfg.provision.grafana.dashboard [{
           name = "parsedmarc";
           options.path = "${pkgs.python3Packages.parsedmarc.dashboard}";
         }];
@@ -539,8 +539,6 @@ in
     };
   };
 
-  # Don't edit the docbook xml directly, edit the md and generate it:
-  # `pandoc parsedmarc.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > parsedmarc.xml`
-  meta.doc = ./parsedmarc.xml;
+  meta.doc = ./parsedmarc.md;
   meta.maintainers = [ lib.maintainers.talyz ];
 }
diff --git a/nixos/modules/services/monitoring/parsedmarc.xml b/nixos/modules/services/monitoring/parsedmarc.xml
deleted file mode 100644
index b6a4bcf8ff5..00000000000
--- a/nixos/modules/services/monitoring/parsedmarc.xml
+++ /dev/null
@@ -1,124 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-parsedmarc">
-  <title>parsedmarc</title>
-  <para>
-    <link xlink:href="https://domainaware.github.io/parsedmarc/">parsedmarc</link>
-    is a service which parses incoming
-    <link xlink:href="https://dmarc.org/">DMARC</link> reports and
-    stores or sends them to a downstream service for further analysis.
-    In combination with Elasticsearch, Grafana and the included Grafana
-    dashboard, it provides a handy overview of DMARC reports over time.
-  </para>
-  <section xml:id="module-services-parsedmarc-basic-usage">
-    <title>Basic usage</title>
-    <para>
-      A very minimal setup which reads incoming reports from an external
-      email address and saves them to a local Elasticsearch instance
-      looks like this:
-    </para>
-    <programlisting>
-services.parsedmarc = {
-  enable = true;
-  settings.imap = {
-    host = &quot;imap.example.com&quot;;
-    user = &quot;alice@example.com&quot;;
-    password = &quot;/path/to/imap_password_file&quot;;
-  };
-  provision.geoIp = false; # Not recommended!
-};
-</programlisting>
-    <para>
-      Note that GeoIP provisioning is disabled in the example for
-      simplicity, but should be turned on for fully functional reports.
-    </para>
-  </section>
-  <section xml:id="local-mail">
-    <title>Local mail</title>
-    <para>
-      Instead of watching an external inbox, a local inbox can be
-      automatically provisioned. The recipient’s name is by default set
-      to <literal>dmarc</literal>, but can be configured in
-      <link xlink:href="options.html#opt-services.parsedmarc.provision.localMail.recipientName">services.parsedmarc.provision.localMail.recipientName</link>.
-      You need to add an MX record pointing to the host. More
-      concretely: for the example to work, an MX record needs to be set
-      up for <literal>monitoring.example.com</literal> and the complete
-      email address that should be configured in the domain’s dmarc
-      policy is <literal>dmarc@monitoring.example.com</literal>.
-    </para>
-    <programlisting>
-services.parsedmarc = {
-  enable = true;
-  provision = {
-    localMail = {
-      enable = true;
-      hostname = monitoring.example.com;
-    };
-    geoIp = false; # Not recommended!
-  };
-};
-</programlisting>
-  </section>
-  <section xml:id="grafana-and-geoip">
-    <title>Grafana and GeoIP</title>
-    <para>
-      The reports can be visualized and summarized with parsedmarc’s
-      official Grafana dashboard. For all views to work, and for the
-      data to be complete, GeoIP databases are also required. The
-      following example shows a basic deployment where the provisioned
-      Elasticsearch instance is automatically added as a Grafana
-      datasource, and the dashboard is added to Grafana as well.
-    </para>
-    <programlisting>
-services.parsedmarc = {
-  enable = true;
-  provision = {
-    localMail = {
-      enable = true;
-      hostname = url;
-    };
-    grafana = {
-      datasource = true;
-      dashboard = true;
-    };
-  };
-};
-
-# Not required, but recommended for full functionality
-services.geoipupdate = {
-  settings = {
-    AccountID = 000000;
-    LicenseKey = &quot;/path/to/license_key_file&quot;;
-  };
-};
-
-services.grafana = {
-  enable = true;
-  addr = &quot;0.0.0.0&quot;;
-  domain = url;
-  rootUrl = &quot;https://&quot; + url;
-  protocol = &quot;socket&quot;;
-  security = {
-    adminUser = &quot;admin&quot;;
-    adminPasswordFile = &quot;/path/to/admin_password_file&quot;;
-    secretKeyFile = &quot;/path/to/secret_key_file&quot;;
-  };
-};
-
-services.nginx = {
-  enable = true;
-  recommendedTlsSettings = true;
-  recommendedOptimisation = true;
-  recommendedGzipSettings = true;
-  recommendedProxySettings = true;
-  upstreams.grafana.servers.&quot;unix:/${config.services.grafana.socket}&quot; = {};
-  virtualHosts.${url} = {
-    root = config.services.grafana.staticRootPath;
-    enableACME = true;
-    forceSSL = true;
-    locations.&quot;/&quot;.tryFiles = &quot;$uri @grafana&quot;;
-    locations.&quot;@grafana&quot;.proxyPass = &quot;http://grafana&quot;;
-  };
-};
-users.users.nginx.extraGroups = [ &quot;grafana&quot; ];
-</programlisting>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/monitoring/prometheus/alertmanager-irc-relay.nix b/nixos/modules/services/monitoring/prometheus/alertmanager-irc-relay.nix
new file mode 100644
index 00000000000..b81d5f6db5e
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/alertmanager-irc-relay.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.alertmanagerIrcRelay;
+
+  configFormat = pkgs.formats.yaml { };
+  configFile = configFormat.generate "alertmanager-irc-relay.yml" cfg.settings;
+in
+{
+  options.services.prometheus.alertmanagerIrcRelay = {
+    enable = mkEnableOption (mdDoc "Alertmanager IRC Relay");
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.alertmanager-irc-relay;
+      defaultText = literalExpression "pkgs.alertmanager-irc-relay";
+      description = mdDoc "Alertmanager IRC Relay package to use.";
+    };
+
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = mdDoc "Extra command line options to pass to alertmanager-irc-relay.";
+    };
+
+    settings = mkOption {
+      type = configFormat.type;
+      example = literalExpression ''
+        {
+          http_host = "localhost";
+          http_port = 8000;
+
+          irc_host = "irc.example.com";
+          irc_port = 7000;
+          irc_nickname = "myalertbot";
+
+          irc_channels = [
+            { name = "#mychannel"; }
+          ];
+        }
+      '';
+      description = mdDoc ''
+        Configuration for Alertmanager IRC Relay as a Nix attribute set.
+        For a reference, check out the
+        [example configuration](https://github.com/google/alertmanager-irc-relay#configuring-and-running-the-bot)
+        and the
+        [source code](https://github.com/google/alertmanager-irc-relay/blob/master/config.go).
+
+        Note: The webhook's URL MUST point to the IRC channel where the message
+        should be posted. For `#mychannel` from the example, this would be
+        `http://localhost:8080/mychannel`.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.alertmanager-irc-relay = {
+      description = "Alertmanager IRC Relay";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/alertmanager-irc-relay \
+          -config ${configFile} \
+          ${escapeShellArgs cfg.extraFlags}
+        '';
+
+        DynamicUser = true;
+        NoNewPrivileges = true;
+
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        ProtectHome = "tmpfs";
+
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateIPC = true;
+
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation"
+          "~@privileged"
+          "~@reboot"
+          "~@setuid"
+          "~@swap"
+        ];
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.oxzi ];
+}
diff --git a/nixos/modules/services/monitoring/prometheus/alertmanager.nix b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
index 0c0931d3d29..987f17c2c6e 100644
--- a/nixos/modules/services/monitoring/prometheus/alertmanager.nix
+++ b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
@@ -6,10 +6,12 @@ let
   cfg = config.services.prometheus.alertmanager;
   mkConfigFile = pkgs.writeText "alertmanager.yml" (builtins.toJSON cfg.configuration);
 
-  checkedConfig = file: pkgs.runCommand "checked-config" { buildInputs = [ cfg.package ]; } ''
-    ln -s ${file} $out
-    amtool check-config $out
-  '';
+  checkedConfig = file:
+    if cfg.checkConfig then
+      pkgs.runCommand "checked-config" { buildInputs = [ cfg.package ]; } ''
+        ln -s ${file} $out
+        amtool check-config $out
+      '' else file;
 
   alertmanagerYml = let
     yml = if cfg.configText != null then
@@ -70,6 +72,20 @@ in {
         '';
       };
 
+      checkConfig = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc ''
+          Check configuration with `amtool check-config`. The call to `amtool` is
+          subject to sandboxing by Nix.
+
+          If you use credentials stored in external files
+          (`environmentFile`, etc),
+          they will not be visible to `amtool`
+          and it will report errors, despite a correct configuration.
+        '';
+      };
+
       logFormat = mkOption {
         type = types.nullOr types.str;
         default = null;
diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix
index f516b75ab10..19ee3ae6f7d 100644
--- a/nixos/modules/services/monitoring/prometheus/default.nix
+++ b/nixos/modules/services/monitoring/prometheus/default.nix
@@ -31,7 +31,7 @@ let
     if checkConfigEnabled then
       pkgs.runCommandLocal
         "${name}-${replaceStrings [" "] [""] what}-checked"
-        { buildInputs = [ cfg.package ]; } ''
+        { buildInputs = [ cfg.package.cli ]; } ''
         ln -s ${file} $out
         promtool ${what} $out
       '' else file;
@@ -1408,7 +1408,7 @@ let
       '';
 
       action =
-        mkDefOpt (types.enum [ "replace" "keep" "drop" "hashmod" "labelmap" "labeldrop" "labelkeep" ]) "replace" ''
+        mkDefOpt (types.enum [ "replace" "lowercase" "uppercase" "keep" "drop" "hashmod" "labelmap" "labeldrop" "labelkeep" ]) "replace" ''
           Action to perform based on regex matching.
         '';
     };
@@ -1614,7 +1614,7 @@ in
 
         The following property holds: switching to a configuration
         (`switch-to-configuration`) that changes the prometheus
-        configuration only finishes successully when prometheus has finished
+        configuration only finishes successfully when prometheus has finished
         loading the new configuration.
       '';
     };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.md b/nixos/modules/services/monitoring/prometheus/exporters.md
new file mode 100644
index 00000000000..34fadecadc7
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters.md
@@ -0,0 +1,180 @@
+# Prometheus exporters {#module-services-prometheus-exporters}
+
+Prometheus exporters provide metrics for the
+[prometheus monitoring system](https://prometheus.io).
+
+## Configuration {#module-services-prometheus-exporters-configuration}
+
+One of the most common exporters is the
+[node exporter](https://github.com/prometheus/node_exporter),
+it provides hardware and OS metrics from the host it's
+running on. The exporter could be configured as follows:
+```
+  services.prometheus.exporters.node = {
+    enable = true;
+    port = 9100;
+    enabledCollectors = [
+      "logind"
+      "systemd"
+    ];
+    disabledCollectors = [
+      "textfile"
+    ];
+    openFirewall = true;
+    firewallFilter = "-i br0 -p tcp -m tcp --dport 9100";
+  };
+```
+It should now serve all metrics from the collectors that are explicitly
+enabled and the ones that are
+[enabled by default](https://github.com/prometheus/node_exporter#enabled-by-default),
+via http under `/metrics`. In this
+example the firewall should just allow incoming connections to the
+exporter's port on the bridge interface `br0` (this would
+have to be configured separately of course). For more information about
+configuration see `man configuration.nix` or search through
+the [available options](https://nixos.org/nixos/options.html#prometheus.exporters).
+
+Prometheus can now be configured to consume the metrics produced by the exporter:
+```
+    services.prometheus = {
+      # ...
+
+      scrapeConfigs = [
+        {
+          job_name = "node";
+          static_configs = [{
+            targets = [ "localhost:${toString config.services.prometheus.exporters.node.port}" ];
+          }];
+        }
+      ];
+
+      # ...
+    }
+```
+
+## Adding a new exporter {#module-services-prometheus-exporters-new-exporter}
+
+To add a new exporter, it has to be packaged first (see
+`nixpkgs/pkgs/servers/monitoring/prometheus/` for
+examples), then a module can be added. The postfix exporter is used in this
+example:
+
+  - Some default options for all exporters are provided by
+    `nixpkgs/nixos/modules/services/monitoring/prometheus/exporters.nix`:
+
+      - `enable`
+      - `port`
+      - `listenAddress`
+      - `extraFlags`
+      - `openFirewall`
+      - `firewallFilter`
+      - `user`
+      - `group`
+  - As there is already a package available, the module can now be added. This
+    is accomplished by adding a new file to the
+    `nixos/modules/services/monitoring/prometheus/exporters/`
+    directory, which will be called postfix.nix and contains all exporter
+    specific options and configuration:
+    ```
+    # nixpkgs/nixos/modules/services/prometheus/exporters/postfix.nix
+    { config, lib, pkgs, options }:
+
+    with lib;
+
+    let
+      # for convenience we define cfg here
+      cfg = config.services.prometheus.exporters.postfix;
+    in
+    {
+      port = 9154; # The postfix exporter listens on this port by default
+
+      # `extraOpts` is an attribute set which contains additional options
+      # (and optional overrides for default options).
+      # Note that this attribute is optional.
+      extraOpts = {
+        telemetryPath = mkOption {
+          type = types.str;
+          default = "/metrics";
+          description = ''
+            Path under which to expose metrics.
+          '';
+        };
+        logfilePath = mkOption {
+          type = types.path;
+          default = /var/log/postfix_exporter_input.log;
+          example = /var/log/mail.log;
+          description = ''
+            Path where Postfix writes log entries.
+            This file will be truncated by this exporter!
+          '';
+        };
+        showqPath = mkOption {
+          type = types.path;
+          default = /var/spool/postfix/public/showq;
+          example = /var/lib/postfix/queue/public/showq;
+          description = ''
+            Path at which Postfix places its showq socket.
+          '';
+        };
+      };
+
+      # `serviceOpts` is an attribute set which contains configuration
+      # for the exporter's systemd service. One of
+      # `serviceOpts.script` and `serviceOpts.serviceConfig.ExecStart`
+      # has to be specified here. This will be merged with the default
+      # service configuration.
+      # Note that by default 'DynamicUser' is 'true'.
+      serviceOpts = {
+        serviceConfig = {
+          DynamicUser = false;
+          ExecStart = ''
+            ${pkgs.prometheus-postfix-exporter}/bin/postfix_exporter \
+              --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+              --web.telemetry-path ${cfg.telemetryPath} \
+              ${concatStringsSep " \\\n  " cfg.extraFlags}
+          '';
+        };
+      };
+    }
+    ```
+  - This should already be enough for the postfix exporter. Additionally one
+    could now add assertions and conditional default values. This can be done
+    in the 'meta-module' that combines all exporter definitions and generates
+    the submodules:
+    `nixpkgs/nixos/modules/services/prometheus/exporters.nix`
+
+## Updating an exporter module {#module-services-prometheus-exporters-update-exporter-module}
+
+Should an exporter option change at some point, it is possible to add
+information about the change to the exporter definition similar to
+`nixpkgs/nixos/modules/rename.nix`:
+```
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.nginx;
+in
+{
+  port = 9113;
+  extraOpts = {
+    # additional module options
+    # ...
+  };
+  serviceOpts = {
+    # service configuration
+    # ...
+  };
+  imports = [
+    # 'services.prometheus.exporters.nginx.telemetryEndpoint' -> 'services.prometheus.exporters.nginx.telemetryPath'
+    (mkRenamedOptionModule [ "telemetryEndpoint" ] [ "telemetryPath" ])
+
+    # removed option 'services.prometheus.exporters.nginx.insecure'
+    (mkRemovedOptionModule [ "insecure" ] ''
+      This option was replaced by 'prometheus.exporters.nginx.sslVerify' which defaults to true.
+    '')
+    ({ options.warnings = options.warnings; })
+  ];
+}
+```
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index f3fbfb149ad..397125b5123 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -35,10 +35,12 @@ let
     "dovecot"
     "fastly"
     "fritzbox"
+    "graphite"
     "influxdb"
     "ipmi"
     "json"
     "jitsi"
+    "junos-czerwonk"
     "kea"
     "keylight"
     "knot"
@@ -54,6 +56,7 @@ let
     "nut"
     "openldap"
     "openvpn"
+    "php-fpm"
     "pihole"
     "postfix"
     "postgres"
@@ -63,7 +66,9 @@ let
     "redis"
     "rspamd"
     "rtl_433"
+    "scaphandre"
     "script"
+    "shelly"
     "snmp"
     "smartctl"
     "smokeping"
@@ -298,6 +303,21 @@ in
         Please specify either 'services.prometheus.exporters.sql.configuration' or
           'services.prometheus.exporters.sql.configFile'
       '';
+    } {
+      assertion = cfg.scaphandre.enable -> (pkgs.stdenv.targetPlatform.isx86_64 == true);
+      message = ''
+        Scaphandre only support x86_64 architectures.
+      '';
+    } {
+      assertion = cfg.scaphandre.enable -> ((lib.kernel.whenHelpers pkgs.linux.version).whenOlder "5.11" true).condition == false;
+      message = ''
+        Scaphandre requires a kernel version newer than '5.11', '${pkgs.linux.version}' given.
+      '';
+    } {
+      assertion = cfg.scaphandre.enable -> (builtins.elem "intel_rapl_common" config.boot.kernelModules);
+      message = ''
+        Scaphandre needs 'intel_rapl_common' kernel module to be enabled. Please add it in 'boot.kernelModules'.
+      '';
     } ] ++ (flip map (attrNames exporterOpts) (exporter: {
       assertion = cfg.${exporter}.firewallFilter != null -> cfg.${exporter}.openFirewall;
       message = ''
@@ -323,7 +343,7 @@ in
   );
 
   meta = {
-    doc = ./exporters.xml;
+    doc = ./exporters.md;
     maintainers = [ maintainers.willibutz ];
   };
 }
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.xml b/nixos/modules/services/monitoring/prometheus/exporters.xml
deleted file mode 100644
index e922e1ace8d..00000000000
--- a/nixos/modules/services/monitoring/prometheus/exporters.xml
+++ /dev/null
@@ -1,248 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-prometheus-exporters">
- <title>Prometheus exporters</title>
- <para>
-  Prometheus exporters provide metrics for the
-  <link xlink:href="https://prometheus.io">prometheus monitoring system</link>.
- </para>
- <section xml:id="module-services-prometheus-exporters-configuration">
-  <title>Configuration</title>
-
-  <para>
-   One of the most common exporters is the
-   <link xlink:href="https://github.com/prometheus/node_exporter">node
-   exporter</link>, it provides hardware and OS metrics from the host it's
-   running on. The exporter could be configured as follows:
-<programlisting>
-  services.prometheus.exporters.node = {
-    enable = true;
-    port = 9100;
-    enabledCollectors = [
-      "logind"
-      "systemd"
-    ];
-    disabledCollectors = [
-      "textfile"
-    ];
-    openFirewall = true;
-    firewallFilter = "-i br0 -p tcp -m tcp --dport 9100";
-  };
-</programlisting>
-   It should now serve all metrics from the collectors that are explicitly
-   enabled and the ones that are
-   <link xlink:href="https://github.com/prometheus/node_exporter#enabled-by-default">enabled
-   by default</link>, via http under <literal>/metrics</literal>. In this
-   example the firewall should just allow incoming connections to the
-   exporter's port on the bridge interface <literal>br0</literal> (this would
-   have to be configured separately of course). For more information about
-   configuration see <literal>man configuration.nix</literal> or search through
-   the
-   <link xlink:href="https://nixos.org/nixos/options.html#prometheus.exporters">available
-   options</link>.
-  </para>
-
-  <para>
-    Prometheus can now be configured to consume the metrics produced by the exporter:
-    <programlisting>
-    services.prometheus = {
-      # ...
-
-      scrapeConfigs = [
-        {
-          job_name = "node";
-          static_configs = [{
-            targets = [ "localhost:${toString config.services.prometheus.exporters.node.port}" ];
-          }];
-        }
-      ];
-
-      # ...
-    }
-    </programlisting>
-  </para>
- </section>
- <section xml:id="module-services-prometheus-exporters-new-exporter">
-  <title>Adding a new exporter</title>
-
-  <para>
-   To add a new exporter, it has to be packaged first (see
-   <literal>nixpkgs/pkgs/servers/monitoring/prometheus/</literal> for
-   examples), then a module can be added. The postfix exporter is used in this
-   example:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Some default options for all exporters are provided by
-     <literal>nixpkgs/nixos/modules/services/monitoring/prometheus/exporters.nix</literal>:
-    </para>
-   </listitem>
-   <listitem override='none'>
-    <itemizedlist>
-     <listitem>
-      <para>
-       <literal>enable</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>port</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>listenAddress</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>extraFlags</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>openFirewall</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>firewallFilter</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>user</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>group</literal>
-      </para>
-     </listitem>
-    </itemizedlist>
-   </listitem>
-   <listitem>
-    <para>
-     As there is already a package available, the module can now be added. This
-     is accomplished by adding a new file to the
-     <literal>nixos/modules/services/monitoring/prometheus/exporters/</literal>
-     directory, which will be called postfix.nix and contains all exporter
-     specific options and configuration:
-<programlisting>
-# nixpgs/nixos/modules/services/prometheus/exporters/postfix.nix
-{ config, lib, pkgs, options }:
-
-with lib;
-
-let
-  # for convenience we define cfg here
-  cfg = config.services.prometheus.exporters.postfix;
-in
-{
-  port = 9154; # The postfix exporter listens on this port by default
-
-  # `extraOpts` is an attribute set which contains additional options
-  # (and optional overrides for default options).
-  # Note that this attribute is optional.
-  extraOpts = {
-    telemetryPath = mkOption {
-      type = types.str;
-      default = "/metrics";
-      description = ''
-        Path under which to expose metrics.
-      '';
-    };
-    logfilePath = mkOption {
-      type = types.path;
-      default = /var/log/postfix_exporter_input.log;
-      example = /var/log/mail.log;
-      description = ''
-        Path where Postfix writes log entries.
-        This file will be truncated by this exporter!
-      '';
-    };
-    showqPath = mkOption {
-      type = types.path;
-      default = /var/spool/postfix/public/showq;
-      example = /var/lib/postfix/queue/public/showq;
-      description = ''
-        Path at which Postfix places its showq socket.
-      '';
-    };
-  };
-
-  # `serviceOpts` is an attribute set which contains configuration
-  # for the exporter's systemd service. One of
-  # `serviceOpts.script` and `serviceOpts.serviceConfig.ExecStart`
-  # has to be specified here. This will be merged with the default
-  # service configuration.
-  # Note that by default 'DynamicUser' is 'true'.
-  serviceOpts = {
-    serviceConfig = {
-      DynamicUser = false;
-      ExecStart = ''
-        ${pkgs.prometheus-postfix-exporter}/bin/postfix_exporter \
-          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
-          --web.telemetry-path ${cfg.telemetryPath} \
-          ${concatStringsSep " \\\n  " cfg.extraFlags}
-      '';
-    };
-  };
-}
-</programlisting>
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     This should already be enough for the postfix exporter. Additionally one
-     could now add assertions and conditional default values. This can be done
-     in the 'meta-module' that combines all exporter definitions and generates
-     the submodules:
-     <literal>nixpkgs/nixos/modules/services/prometheus/exporters.nix</literal>
-    </para>
-   </listitem>
-  </itemizedlist>
- </section>
- <section xml:id="module-services-prometheus-exporters-update-exporter-module">
-  <title>Updating an exporter module</title>
-   <para>
-     Should an exporter option change at some point, it is possible to add
-     information about the change to the exporter definition similar to
-     <literal>nixpkgs/nixos/modules/rename.nix</literal>:
-<programlisting>
-{ config, lib, pkgs, options }:
-
-with lib;
-
-let
-  cfg = config.services.prometheus.exporters.nginx;
-in
-{
-  port = 9113;
-  extraOpts = {
-    # additional module options
-    # ...
-  };
-  serviceOpts = {
-    # service configuration
-    # ...
-  };
-  imports = [
-    # 'services.prometheus.exporters.nginx.telemetryEndpoint' -> 'services.prometheus.exporters.nginx.telemetryPath'
-    (mkRenamedOptionModule [ "telemetryEndpoint" ] [ "telemetryPath" ])
-
-    # removed option 'services.prometheus.exporters.nginx.insecure'
-    (mkRemovedOptionModule [ "insecure" ] ''
-      This option was replaced by 'prometheus.exporters.nginx.sslVerify' which defaults to true.
-    '')
-    ({ options.warnings = options.warnings; })
-  ];
-}
-</programlisting>
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
index 0c2de683ecf..f67596f05a3 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
@@ -58,10 +58,10 @@ in
     };
   };
   serviceOpts = let
-    collectSettingsArgs = if (cfg.collectdBinary.enable) then ''
+    collectSettingsArgs = optionalString (cfg.collectdBinary.enable) ''
       --collectd.listen-address ${cfg.collectdBinary.listenAddress}:${toString cfg.collectdBinary.port} \
       --collectd.security-level ${cfg.collectdBinary.securityLevel} \
-    '' else "";
+    '';
   in {
     serviceConfig = {
       ExecStart = ''
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/graphite.nix b/nixos/modules/services/monitoring/prometheus/exporters/graphite.nix
new file mode 100644
index 00000000000..34a88710421
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/graphite.nix
@@ -0,0 +1,41 @@
+{ config, lib, pkgs, options }:
+
+let
+  cfg = config.services.prometheus.exporters.graphite;
+  format = pkgs.formats.yaml { };
+in
+{
+  port = 9108;
+  extraOpts = {
+    graphitePort = lib.mkOption {
+      type = lib.types.port;
+      default = 9109;
+      description = lib.mdDoc ''
+        Port to use for the graphite server.
+      '';
+    };
+    mappingSettings = lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = format.type;
+        options = { };
+      };
+      default = { };
+      description = lib.mdDoc ''
+        Mapping configuration for the exporter, see
+        <https://github.com/prometheus/graphite_exporter#yaml-config> for
+        available options.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-graphite-exporter}/bin/graphite_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --graphite.listen-address ${cfg.listenAddress}:${toString cfg.graphitePort} \
+          --graphite.mapping-config ${format.generate "mapping.yml" cfg.mappingSettings} \
+          ${lib.concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/junos-czerwonk.nix b/nixos/modules/services/monitoring/prometheus/exporters/junos-czerwonk.nix
new file mode 100644
index 00000000000..15e0c9ecb17
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/junos-czerwonk.nix
@@ -0,0 +1,72 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.junos-czerwonk;
+
+  configFile = if cfg.configuration != null then configurationFile else (escapeShellArg cfg.configurationFile);
+
+  configurationFile = pkgs.writeText "prometheus-junos-czerwonk-exporter.conf" (builtins.toJSON (cfg.configuration));
+in
+{
+  port = 9326;
+  extraOpts = {
+    environmentFile = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = lib.mdDoc ''
+        File containing env-vars to be substituted into the exporter's config.
+      '';
+    };
+    configurationFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = lib.mdDoc ''
+        Specify the JunOS exporter configuration file to use.
+      '';
+    };
+    configuration = mkOption {
+      type = types.nullOr types.attrs;
+      default = null;
+      description = lib.mdDoc ''
+        JunOS exporter configuration as nix attribute set. Mutually exclusive with the `configurationFile` option.
+      '';
+      example = {
+        devices = [
+          {
+            host = "router1";
+            key_file = "/path/to/key";
+          }
+        ];
+      };
+    };
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = lib.mdDoc ''
+        Path under which to expose metrics.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = false;
+      EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
+      RuntimeDirectory = "prometheus-junos-czerwonk-exporter";
+      ExecStartPre = [
+        "${pkgs.writeShellScript "subst-secrets-junos-czerwonk-exporter" ''
+          umask 0077
+          ${pkgs.envsubst}/bin/envsubst -i ${configFile} -o ''${RUNTIME_DIRECTORY}/junos-exporter.json
+        ''}"
+      ];
+      ExecStart = ''
+        ${pkgs.prometheus-junos-czerwonk-exporter}/bin/junos_exporter \
+          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          -web.telemetry-path ${cfg.telemetryPath} \
+          -config.file ''${RUNTIME_DIRECTORY}/junos-exporter.json \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/php-fpm.nix b/nixos/modules/services/monitoring/prometheus/exporters/php-fpm.nix
new file mode 100644
index 00000000000..8f6942002f7
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/php-fpm.nix
@@ -0,0 +1,65 @@
+{ config
+, lib
+, pkgs
+, options
+}:
+
+let
+  logPrefix = "services.prometheus.exporter.php-fpm";
+  cfg = config.services.prometheus.exporters.php-fpm;
+in {
+  port = 9253;
+  extraOpts = {
+    package = lib.mkPackageOptionMD pkgs "prometheus-php-fpm-exporter" {};
+
+    telemetryPath = lib.mkOption {
+      type = lib.types.str;
+      default = "/metrics";
+      description = lib.mdDoc ''
+        Path under which to expose metrics.
+      '';
+    };
+
+    environmentFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      example = "/root/prometheus-php-fpm-exporter.env";
+      description = lib.mdDoc ''
+        Environment file as defined in {manpage}`systemd.exec(5)`.
+
+        Secrets may be passed to the service without adding them to the
+        world-readable Nix store, by specifying placeholder variables as
+        the option value in Nix and setting these variables accordingly in the
+        environment file.
+
+        Environment variables from this file will be interpolated into the
+        config file using envsubst with this syntax:
+        `$ENVIRONMENT ''${VARIABLE}`
+
+        For variables to use see [options and defaults](https://github.com/hipages/php-fpm_exporter#options-and-defaults).
+
+        The main use is to set the PHP_FPM_SCRAPE_URI that indicate how to connect to PHP-FPM process.
+
+        ```
+          # Content of the environment file
+          PHP_FPM_SCRAPE_URI="unix:///tmp/php.sock;/status"
+        ```
+
+        Note that this file needs to be available on the host on which
+        this exporter is running.
+      '';
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
+      ExecStart = ''
+        ${lib.getExe cfg.package} server \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path ${cfg.telemetryPath} \
+          ${lib.concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix b/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix
index 537d72e85c8..6f403b3e58c 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix
@@ -6,6 +6,11 @@ let
   cfg = config.services.prometheus.exporters.pihole;
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "interval"] "This option has been removed.")
+    ({ options.warnings = options.warnings; options.assertions = options.assertions; })
+  ];
+
   port = 9617;
   extraOpts = {
     apiToken = mkOption {
@@ -13,15 +18,7 @@ in
       default = "";
       example = "580a770cb40511eb85290242ac130003580a770cb40511eb85290242ac130003";
       description = lib.mdDoc ''
-        pi-hole API token which can be used instead of a password
-      '';
-    };
-    interval = mkOption {
-      type = types.str;
-      default = "10s";
-      example = "30s";
-      description = lib.mdDoc ''
-        How often to scrape new data
+        Pi-Hole API token which can be used instead of a password
       '';
     };
     password = mkOption {
@@ -29,7 +26,7 @@ in
       default = "";
       example = "password";
       description = lib.mdDoc ''
-        The password to login into pihole. An api token can be used instead.
+        The password to login into Pi-Hole. An api token can be used instead.
       '';
     };
     piholeHostname = mkOption {
@@ -37,7 +34,7 @@ in
       default = "pihole";
       example = "127.0.0.1";
       description = lib.mdDoc ''
-        Hostname or address where to find the pihole webinterface
+        Hostname or address where to find the Pi-Hole webinterface
       '';
     };
     piholePort = mkOption {
@@ -45,7 +42,7 @@ in
       default = 80;
       example = 443;
       description = lib.mdDoc ''
-        The port pihole webinterface is reachable on
+        The port Pi-Hole webinterface is reachable on
       '';
     };
     protocol = mkOption {
@@ -53,21 +50,28 @@ in
       default = "http";
       example = "https";
       description = lib.mdDoc ''
-        The protocol which is used to connect to pihole
+        The protocol which is used to connect to Pi-Hole
+      '';
+    };
+    timeout = mkOption {
+      type = types.str;
+      default = "5s";
+      description = lib.mdDoc ''
+        Controls the timeout to connect to a Pi-Hole instance
       '';
     };
   };
   serviceOpts = {
     serviceConfig = {
       ExecStart = ''
-        ${pkgs.bash}/bin/bash -c "${pkgs.prometheus-pihole-exporter}/bin/pihole-exporter \
-          -interval ${cfg.interval} \
+        ${pkgs.prometheus-pihole-exporter}/bin/pihole-exporter \
           ${optionalString (cfg.apiToken != "") "-pihole_api_token ${cfg.apiToken}"} \
           -pihole_hostname ${cfg.piholeHostname} \
           ${optionalString (cfg.password != "") "-pihole_password ${cfg.password}"} \
           -pihole_port ${toString cfg.piholePort} \
           -pihole_protocol ${cfg.protocol} \
-          -port ${toString cfg.port}"
+          -port ${toString cfg.port} \
+          -timeout ${cfg.timeout}
       '';
     };
   };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix b/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
index 0b48827f43f..f9dcfad07d3 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
@@ -9,7 +9,7 @@ let
     pkgs.writeText "rspamd-exporter-config.yml" (builtins.toJSON conf);
 
   generateConfig = extraLabels: {
-    metrics = (map (path: {
+    modules.default.metrics = (map (path: {
       name = "rspamd_${replaceStrings [ "[" "." " " "]" "\\" "'" ] [ "_" "_" "_" "" "" "" ] path}";
       path = "{ .${path} }";
       labels = extraLabels;
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/scaphandre.nix b/nixos/modules/services/monitoring/prometheus/exporters/scaphandre.nix
new file mode 100644
index 00000000000..3b6ebf65b09
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/scaphandre.nix
@@ -0,0 +1,33 @@
+{ config
+, lib
+, pkgs
+, options
+}:
+
+let
+  logPrefix = "services.prometheus.exporter.scaphandre";
+  cfg = config.services.prometheus.exporters.scaphandre;
+in {
+  port = 8080;
+  extraOpts = {
+    telemetryPath = lib.mkOption {
+      type = lib.types.str;
+      default = "/metrics";
+      description = lib.mdDoc ''
+        Path under which to expose metrics.
+      '';
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.scaphandre}/bin/scaphandre prometheus \
+          --address ${cfg.listenAddress} \
+          --port ${toString cfg.port} \
+          --suffix ${cfg.telemetryPath} \
+          ${lib.concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/shelly.nix b/nixos/modules/services/monitoring/prometheus/exporters/shelly.nix
new file mode 100644
index 00000000000..b9cfd1b1e84
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/shelly.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.shelly;
+in
+{
+  port = 9784;
+  extraOpts = {
+    metrics-file = mkOption {
+      type = types.path;
+      description = lib.mdDoc ''
+        Path to the JSON file with the metric definitions
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-shelly-exporter}/bin/shelly_exporter \
+          -metrics-file ${cfg.metrics-file} \
+          -listen-address ${cfg.listenAddress}:${toString cfg.port}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
index 0c5648c1414..50e1321a1e9 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
@@ -4,12 +4,12 @@ with lib;
 
 let
   cfg = config.services.prometheus.exporters.smartctl;
-  args = concatStrings [
-    "--web.listen-address=\"${cfg.listenAddress}:${toString cfg.port}\" "
-    "--smartctl.path=\"${pkgs.smartmontools}/bin/smartctl\" "
-    "--smartctl.interval=\"${cfg.maxInterval}\" "
-    "${concatMapStringsSep " " (device: "--smartctl.device=${device}") cfg.devices}"
-  ];
+  args = lib.escapeShellArgs ([
+    "--web.listen-address=${cfg.listenAddress}:${toString cfg.port}"
+    "--smartctl.path=${pkgs.smartmontools}/bin/smartctl"
+    "--smartctl.interval=${cfg.maxInterval}"
+  ] ++ map (device: "--smartctl.device=${device}") cfg.devices
+  ++ cfg.extraFlags);
 in {
   port = 9633;
 
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix b/nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix
index 5cd1e2c65e9..3b7f978528c 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix
@@ -24,9 +24,9 @@ in {
     inherit (options.services.unpoller.unifi) controllers;
     inherit (options.services.unpoller) loki;
     log = {
-      debug = mkEnableOption (lib.mdDoc "debug logging including line numbers, high resolution timestamps, per-device logs.");
-      quiet = mkEnableOption (lib.mdDoc "startup and error logs only.");
-      prometheusErrors = mkEnableOption (lib.mdDoc "emitting errors to prometheus.");
+      debug = mkEnableOption (lib.mdDoc "debug logging including line numbers, high resolution timestamps, per-device logs");
+      quiet = mkEnableOption (lib.mdDoc "startup and error logs only");
+      prometheusErrors = mkEnableOption (lib.mdDoc "emitting errors to prometheus");
     };
   };
 
diff --git a/nixos/modules/services/monitoring/tuptime.nix b/nixos/modules/services/monitoring/tuptime.nix
index d97e408bce3..97cc3752625 100644
--- a/nixos/modules/services/monitoring/tuptime.nix
+++ b/nixos/modules/services/monitoring/tuptime.nix
@@ -54,8 +54,8 @@ in {
             Type = "oneshot";
             User = "_tuptime";
             RemainAfterExit = true;
-            ExecStart = "${pkgs.tuptime}/bin/tuptime -x";
-            ExecStop = "${pkgs.tuptime}/bin/tuptime -xg";
+            ExecStart = "${pkgs.tuptime}/bin/tuptime -q";
+            ExecStop = "${pkgs.tuptime}/bin/tuptime -qg";
           };
         };
 
@@ -64,7 +64,7 @@ in {
           serviceConfig = {
             Type = "oneshot";
             User = "_tuptime";
-            ExecStart = "${pkgs.tuptime}/bin/tuptime -x";
+            ExecStart = "${pkgs.tuptime}/bin/tuptime -q";
           };
         };
       };
diff --git a/nixos/modules/services/monitoring/unpoller.nix b/nixos/modules/services/monitoring/unpoller.nix
index f0ced5513d6..557e2bff4c2 100644
--- a/nixos/modules/services/monitoring/unpoller.nix
+++ b/nixos/modules/services/monitoring/unpoller.nix
@@ -47,7 +47,7 @@ in {
         type = types.bool;
         default = false;
         description = lib.mdDoc ''
-          Whether to disable the prometheus ouput plugin.
+          Whether to disable the prometheus output plugin.
         '';
       };
       http_listen = mkOption {
@@ -71,7 +71,7 @@ in {
         type = types.bool;
         default = false;
         description = lib.mdDoc ''
-          Whether to disable the influxdb ouput plugin.
+          Whether to disable the influxdb output plugin.
         '';
       };
       url = mkOption {
diff --git a/nixos/modules/services/monitoring/uptime-kuma.nix b/nixos/modules/services/monitoring/uptime-kuma.nix
index 8e6c825b35e..7027046b242 100644
--- a/nixos/modules/services/monitoring/uptime-kuma.nix
+++ b/nixos/modules/services/monitoring/uptime-kuma.nix
@@ -7,9 +7,11 @@ let
 in
 {
 
+  meta.maintainers = [ lib.maintainers.julienmalka ];
+
   options = {
     services.uptime-kuma = {
-      enable = mkEnableOption (mdDoc "Uptime Kuma, this assumes a reverse proxy to be set.");
+      enable = mkEnableOption (mdDoc "Uptime Kuma, this assumes a reverse proxy to be set");
 
       package = mkOption {
         type = types.package;
@@ -18,9 +20,10 @@ in
         description = lib.mdDoc "Uptime Kuma package to use.";
       };
 
+      appriseSupport = mkEnableOption (mdDoc "apprise support for notifications");
+
       settings = lib.mkOption {
-        type =
-          lib.types.submodule { freeformType = with lib.types; attrsOf str; };
+        type = lib.types.submodule { freeformType = with lib.types; attrsOf str; };
         default = { };
         example = {
           PORT = "4000";
@@ -40,6 +43,8 @@ in
     services.uptime-kuma.settings = {
       DATA_DIR = "/var/lib/uptime-kuma/";
       NODE_ENV = mkDefault "production";
+      HOST = mkDefault "127.0.0.1";
+      PORT = mkDefault "3001";
     };
 
     systemd.services.uptime-kuma = {
@@ -47,6 +52,7 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       environment = cfg.settings;
+      path = with pkgs; [ unixtools.ping ] ++ lib.optional cfg.appriseSupport apprise;
       serviceConfig = {
         Type = "simple";
         StateDirectory = "uptime-kuma";
diff --git a/nixos/modules/services/monitoring/vmalert.nix b/nixos/modules/services/monitoring/vmalert.nix
new file mode 100644
index 00000000000..27fb34e199b
--- /dev/null
+++ b/nixos/modules/services/monitoring/vmalert.nix
@@ -0,0 +1,136 @@
+{ config, pkgs, lib, ... }: with lib;
+let
+  cfg = config.services.vmalert;
+
+  format = pkgs.formats.yaml {};
+
+  confOpts = concatStringsSep " \\\n" (mapAttrsToList mkLine (filterAttrs (_: v: v != false) cfg.settings));
+  confType = with types;
+    let
+      valueType = oneOf [ bool int path str ];
+    in
+    attrsOf (either valueType (listOf valueType));
+
+  mkLine = key: value:
+    if value == true then "-${key}"
+    else if isList value then concatMapStringsSep " " (v: "-${key}=${escapeShellArg (toString v)}") value
+    else "-${key}=${escapeShellArg (toString value)}"
+  ;
+in
+{
+  # interface
+  options.services.vmalert = {
+    enable = mkEnableOption (mdDoc "vmalert");
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.victoriametrics;
+      defaultText = "pkgs.victoriametrics";
+      description = mdDoc ''
+        The VictoriaMetrics derivation to use.
+      '';
+    };
+
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = confType;
+        options = {
+
+          "datasource.url" = mkOption {
+            type = types.nonEmptyStr;
+            example = "http://localhost:8428";
+            description = mdDoc ''
+              Datasource compatible with Prometheus HTTP API.
+            '';
+          };
+
+          "notifier.url" = mkOption {
+            type = with types; listOf nonEmptyStr;
+            default = [];
+            example = [ "http://127.0.0.1:9093" ];
+            description = mdDoc ''
+              Prometheus Alertmanager URL. List all Alertmanager URLs if it runs in the cluster mode to ensure high availability.
+            '';
+          };
+
+          "rule" = mkOption {
+            type = with types; listOf path;
+            description = mdDoc ''
+              Path to the files with alerting and/or recording rules.
+
+              ::: {.note}
+              Consider using the {option}`services.vmalert.rules` option as a convenient alternative for declaring rules
+              directly in the `nix` language.
+              :::
+            '';
+          };
+
+        };
+      };
+      default = { };
+      example = {
+        "datasource.url" = "http://localhost:8428";
+        "datasource.disableKeepAlive" = true;
+        "datasource.showURL" = false;
+        "rule" = [
+          "http://<some-server-addr>/path/to/rules"
+          "dir/*.yaml"
+        ];
+      };
+      description = mdDoc ''
+        `vmalert` configuration, passed via command line flags. Refer to
+        <https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmalert/README.md#configuration>
+        for details on supported values.
+      '';
+    };
+
+    rules = mkOption {
+      type = format.type;
+      default = {};
+      example = {
+        group = [
+          { name = "TestGroup";
+            rules = [
+              { alert = "ExampleAlertAlwaysFiring";
+                expr = ''
+                  sum by(job)
+                  (up == 1)
+                '';
+              }
+            ];
+          }
+        ];
+      };
+      description = mdDoc ''
+        A list of the given alerting or recording rules against configured `"datasource.url"` compatible with
+        Prometheus HTTP API for `vmalert` to execute. Refer to
+        <https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmalert/README.md#rules>
+        for details on supported values.
+      '';
+    };
+  };
+
+  # implementation
+  config = mkIf cfg.enable {
+
+    environment.etc."vmalert/rules.yml".source = format.generate "rules.yml" cfg.rules;
+
+    services.vmalert.settings.rule = [
+      "/etc/vmalert/rules.yml"
+    ];
+
+    systemd.services.vmalert = {
+      description = "vmalert service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      reloadTriggers = [ config.environment.etc."vmalert/rules.yml".source ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "on-failure";
+        ExecStart = "${cfg.package}/bin/vmalert ${confOpts}";
+        ExecReload = ''${pkgs.coreutils}/bin/kill -SIGHUP "$MAINPID"'';
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/ceph.nix b/nixos/modules/services/network-filesystems/ceph.nix
index 22d58f29cb8..aad03728b20 100644
--- a/nixos/modules/services/network-filesystems/ceph.nix
+++ b/nixos/modules/services/network-filesystems/ceph.nix
@@ -3,18 +3,18 @@
 with lib;
 
 let
-  cfg  = config.services.ceph;
+  cfg = config.services.ceph;
 
   # function that translates "camelCaseOptions" to "camel case options", credits to tilpner in #nixos@freenode
   expandCamelCase = replaceStrings upperChars (map (s: " ${s}") lowerChars);
   expandCamelCaseAttrs = mapAttrs' (name: value: nameValuePair (expandCamelCase name) value);
 
-  makeServices = (daemonType: daemonIds:
+  makeServices = daemonType: daemonIds:
     mkMerge (map (daemonId:
-      { "ceph-${daemonType}-${daemonId}" = makeService daemonType daemonId cfg.global.clusterName pkgs.ceph; })
-      daemonIds));
+      { "ceph-${daemonType}-${daemonId}" = makeService daemonType daemonId cfg.global.clusterName cfg.${daemonType}.package; })
+      daemonIds);
 
-  makeService = (daemonType: daemonId: clusterName: ceph:
+  makeService = daemonType: daemonId: clusterName: ceph:
     let
       stateDirectory = "ceph/${if daemonType == "rgw" then "radosgw" else daemonType}/${clusterName}-${daemonId}"; in {
     enable = true;
@@ -54,9 +54,9 @@ let
     } // optionalAttrs ( daemonType == "mon") {
       RestartSec = "10";
     };
-  });
+  };
 
-  makeTarget = (daemonType:
+  makeTarget = daemonType:
     {
       "ceph-${daemonType}" = {
         description = "Ceph target allowing to start/stop all ceph-${daemonType} services at once";
@@ -65,8 +65,7 @@ let
         before = [ "ceph.target" ];
         unitConfig.StopWhenUnneeded = true;
       };
-    }
-  );
+    };
 in
 {
   options.services.ceph = {
@@ -211,6 +210,7 @@ in
           to the id part in ceph i.e. [ "name1" ] would result in mgr.name1
         '';
       };
+      package = mkPackageOptionMD pkgs "ceph" { };
       extraConfig = mkOption {
         type = with types; attrsOf str;
         default = {};
@@ -231,6 +231,7 @@ in
           to the id part in ceph i.e. [ "name1" ] would result in mon.name1
         '';
       };
+      package = mkPackageOptionMD pkgs "ceph" { };
       extraConfig = mkOption {
         type = with types; attrsOf str;
         default = {};
@@ -251,7 +252,7 @@ in
           to the id part in ceph i.e. [ "name1" ] would result in osd.name1
         '';
       };
-
+      package = mkPackageOptionMD pkgs "ceph" { };
       extraConfig = mkOption {
         type = with types; attrsOf str;
         default = {
@@ -279,6 +280,7 @@ in
           to the id part in ceph i.e. [ "name1" ] would result in mds.name1
         '';
       };
+      package = mkPackageOptionMD pkgs "ceph" { };
       extraConfig = mkOption {
         type = with types; attrsOf str;
         default = {};
@@ -290,6 +292,7 @@ in
 
     rgw = {
       enable = mkEnableOption (lib.mdDoc "Ceph RadosGW daemon");
+      package = mkPackageOptionMD pkgs "ceph" { };
       daemons = mkOption {
         type = with types; listOf str;
         default = [];
@@ -328,16 +331,16 @@ in
       { assertion = cfg.global.fsid != "";
         message = "fsid has to be set to a valid uuid for the cluster to function";
       }
-      { assertion = cfg.mon.enable == true -> cfg.mon.daemons != [];
+      { assertion = cfg.mon.enable -> cfg.mon.daemons != [];
         message = "have to set id of atleast one MON if you're going to enable Monitor";
       }
-      { assertion = cfg.mds.enable == true -> cfg.mds.daemons != [];
+      { assertion = cfg.mds.enable -> cfg.mds.daemons != [];
         message = "have to set id of atleast one MDS if you're going to enable Metadata Service";
       }
-      { assertion = cfg.osd.enable == true -> cfg.osd.daemons != [];
+      { assertion = cfg.osd.enable -> cfg.osd.daemons != [];
         message = "have to set id of atleast one OSD if you're going to enable OSD";
       }
-      { assertion = cfg.mgr.enable == true -> cfg.mgr.daemons != [];
+      { assertion = cfg.mgr.enable -> cfg.mgr.daemons != [];
         message = "have to set id of atleast one MGR if you're going to enable MGR";
       }
     ];
diff --git a/nixos/modules/services/network-filesystems/glusterfs.nix b/nixos/modules/services/network-filesystems/glusterfs.nix
index 5c3e197b687..ee03bada492 100644
--- a/nixos/modules/services/network-filesystems/glusterfs.nix
+++ b/nixos/modules/services/network-filesystems/glusterfs.nix
@@ -15,11 +15,11 @@ let
     rm -f /var/lib/glusterd/secure-access
   '';
 
-  restartTriggers = if (cfg.tlsSettings != null) then [
+  restartTriggers = optionals (cfg.tlsSettings != null) [
     config.environment.etc."ssl/glusterfs.pem".source
     config.environment.etc."ssl/glusterfs.key".source
     config.environment.etc."ssl/glusterfs.ca".source
-  ] else [];
+  ];
 
   cfg = config.services.glusterfs;
 
diff --git a/nixos/modules/services/network-filesystems/kubo.nix b/nixos/modules/services/network-filesystems/kubo.nix
index 13a062c3212..a5c370b5be8 100644
--- a/nixos/modules/services/network-filesystems/kubo.nix
+++ b/nixos/modules/services/network-filesystems/kubo.nix
@@ -5,6 +5,35 @@ let
 
   settingsFormat = pkgs.formats.json {};
 
+  rawDefaultConfig = lib.importJSON (pkgs.runCommand "kubo-default-config" {
+    nativeBuildInputs = [ cfg.package ];
+  } ''
+    export IPFS_PATH="$TMPDIR"
+    ipfs init --empty-repo --profile=${profile}
+    ipfs --offline config show > "$out"
+  '');
+
+  # Remove the PeerID (an attribute of "Identity") of the temporary Kubo repo.
+  # The "Pinning" section contains the "RemoteServices" section, which would prevent
+  # the daemon from starting as that setting can't be changed via ipfs config replace.
+  defaultConfig = builtins.removeAttrs rawDefaultConfig [ "Identity" "Pinning" ];
+
+  customizedConfig = lib.recursiveUpdate defaultConfig cfg.settings;
+
+  configFile = settingsFormat.generate "kubo-config.json" customizedConfig;
+
+  # Create a fake repo containing only the file "api".
+  # $IPFS_PATH will point to this directory instead of the real one.
+  # For some reason the Kubo CLI tools insist on reading the
+  # config file when it exists. But the Kubo daemon sets the file
+  # permissions such that only the ipfs user is allowed to read
+  # this file. This prevents normal users from talking to the daemon.
+  # To work around this terrible design, create a fake repo with no
+  # config file, only an api file and everything should work as expected.
+  fakeKuboRepo = pkgs.writeTextDir "api" ''
+    /unix/run/ipfs.sock
+  '';
+
   kuboFlags = utils.escapeSystemdExecArgs (
     optional cfg.autoMount "--mount" ++
     optional cfg.enableGC "--enable-gc" ++
@@ -21,6 +50,22 @@ let
 
   splitMulitaddr = addrRaw: lib.tail (lib.splitString "/" addrRaw);
 
+  multiaddrsToListenStreams = addrIn:
+    let
+      addrs = if builtins.typeOf addrIn == "list"
+      then addrIn else [ addrIn ];
+      unfilteredResult = map multiaddrToListenStream addrs;
+    in
+      builtins.filter (addr: addr != null) unfilteredResult;
+
+  multiaddrsToListenDatagrams = addrIn:
+    let
+      addrs = if builtins.typeOf addrIn == "list"
+      then addrIn else [ addrIn ];
+      unfilteredResult = map multiaddrToListenDatagram addrs;
+    in
+      builtins.filter (addr: addr != null) unfilteredResult;
+
   multiaddrToListenStream = addrRaw:
     let
       addr = splitMulitaddr addrRaw;
@@ -127,8 +172,8 @@ in
 
       emptyRepo = mkOption {
         type = types.bool;
-        default = false;
-        description = lib.mdDoc "If set to true, the repo won't be initialized with help files";
+        default = true;
+        description = lib.mdDoc "If set to false, the repo will be initialized with help files";
       };
 
       settings = mkOption {
@@ -137,13 +182,18 @@ in
 
           options = {
             Addresses.API = mkOption {
-              type = types.str;
-              default = "/ip4/127.0.0.1/tcp/5001";
-              description = lib.mdDoc "Where Kubo exposes its API to";
+              type = types.oneOf [ types.str (types.listOf types.str) ];
+              default = [ ];
+              description = lib.mdDoc ''
+                Multiaddr or array of multiaddrs describing the address to serve the local HTTP API on.
+                In addition to the multiaddrs listed here, the daemon will also listen on a Unix domain socket.
+                To allow the ipfs CLI tools to communicate with the daemon over that socket,
+                add your user to the correct group, e.g. `users.users.alice.extraGroups = [ config.services.kubo.group ];`
+              '';
             };
 
             Addresses.Gateway = mkOption {
-              type = types.str;
+              type = types.oneOf [ types.str (types.listOf types.str) ];
               default = "/ip4/127.0.0.1/tcp/8080";
               description = lib.mdDoc "Where the IPFS Gateway can be reached";
             };
@@ -154,16 +204,20 @@ in
                 "/ip4/0.0.0.0/tcp/4001"
                 "/ip6/::/tcp/4001"
                 "/ip4/0.0.0.0/udp/4001/quic"
+                "/ip4/0.0.0.0/udp/4001/quic-v1"
+                "/ip4/0.0.0.0/udp/4001/quic-v1/webtransport"
                 "/ip6/::/udp/4001/quic"
+                "/ip6/::/udp/4001/quic-v1"
+                "/ip6/::/udp/4001/quic-v1/webtransport"
               ];
               description = lib.mdDoc "Where Kubo listens for incoming p2p connections";
             };
           };
         };
         description = lib.mdDoc ''
-          Attrset of daemon configuration to set using {command}`ipfs config`, every time the daemon starts.
+          Attrset of daemon configuration.
           See [https://github.com/ipfs/kubo/blob/master/docs/config.md](https://github.com/ipfs/kubo/blob/master/docs/config.md) for reference.
-          Keep in mind that this configuration is stateful; i.e., unsetting anything in here does not reset the value to the default!
+          You can't set `Identity` or `Pinning`.
         '';
         default = { };
         example = {
@@ -211,8 +265,23 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !builtins.hasAttr "Identity" cfg.settings;
+        message = ''
+          You can't set services.kubo.settings.Identity because the ``config replace`` subcommand used at startup does not support modifying any of the Identity settings.
+        '';
+      }
+      {
+        assertion = !((builtins.hasAttr "Pinning" cfg.settings) && (builtins.hasAttr "RemoteServices" cfg.settings.Pinning));
+        message = ''
+          You can't set services.kubo.settings.Pinning.RemoteServices because the ``config replace`` subcommand used at startup does not work with it.
+        '';
+      }
+    ];
+
     environment.systemPackages = [ cfg.package ];
-    environment.variables.IPFS_PATH = cfg.dataDir;
+    environment.variables.IPFS_PATH = fakeKuboRepo;
 
     # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
     boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
@@ -262,21 +331,30 @@ in
 
       preStart = ''
         if [[ ! -f "$IPFS_PATH/config" ]]; then
-          ipfs init ${optionalString cfg.emptyRepo "-e"} --profile=${profile}
+          ipfs init --empty-repo=${lib.boolToString cfg.emptyRepo}
         else
           # After an unclean shutdown this file may exist which will cause the config command to attempt to talk to the daemon. This will hang forever if systemd is holding our sockets open.
           rm -vf "$IPFS_PATH/api"
       '' + optionalString cfg.autoMigrate ''
         ${pkgs.kubo-migrator}/bin/fs-repo-migrations -to '${cfg.package.repoVersion}' -y
       '' + ''
-          ipfs --offline config profile apply ${profile} >/dev/null
         fi
-      '' + ''
-        ipfs --offline config show \
-          | ${pkgs.jq}/bin/jq '. * $settings' --argjson settings ${
-              escapeShellArg (builtins.toJSON cfg.settings)
-            } \
-          | ipfs --offline config replace -
+        ipfs --offline config show |
+          ${pkgs.jq}/bin/jq -s '.[0].Pinning as $Pinning | .[0].Identity as $Identity | .[1] + {$Identity,$Pinning}' - '${configFile}' |
+
+          # This command automatically injects the private key and other secrets from
+          # the old config file back into the new config file.
+          # Unfortunately, it doesn't keep the original `Identity.PeerID`,
+          # so we need `ipfs config show` and jq above.
+          # See https://github.com/ipfs/kubo/issues/8993 for progress on fixing this problem.
+          # Kubo also wants a specific version of the original "Pinning.RemoteServices"
+          # section (redacted by `ipfs config show`), such that that section doesn't
+          # change when the changes are applied. Whyyyyyy.....
+          ipfs --offline config replace -
+      '';
+      postStop = mkIf cfg.autoMount ''
+        # After an unclean shutdown the fuse mounts at cfg.ipnsMountDir and cfg.ipfsMountDir are locked
+        umount --quiet '${cfg.ipnsMountDir}' '${cfg.ipfsMountDir}' || true
       '';
       serviceConfig = {
         ExecStart = [ "" "${cfg.package}/bin/ipfs daemon ${kuboFlags}" ];
@@ -293,27 +371,23 @@ in
       wantedBy = [ "sockets.target" ];
       socketConfig = {
         ListenStream =
-          let
-            fromCfg = multiaddrToListenStream cfg.settings.Addresses.Gateway;
-          in
-          [ "" ] ++ lib.optional (fromCfg != null) fromCfg;
+          [ "" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.Gateway);
         ListenDatagram =
-          let
-            fromCfg = multiaddrToListenDatagram cfg.settings.Addresses.Gateway;
-          in
-          [ "" ] ++ lib.optional (fromCfg != null) fromCfg;
+          [ "" ] ++ (multiaddrsToListenDatagrams cfg.settings.Addresses.Gateway);
       };
     };
 
     systemd.sockets.ipfs-api = {
       wantedBy = [ "sockets.target" ];
-      # We also include "%t/ipfs.sock" because there is no way to put the "%t"
-      # in the multiaddr.
-      socketConfig.ListenStream =
-        let
-          fromCfg = multiaddrToListenStream cfg.settings.Addresses.API;
-        in
-        [ "" "%t/ipfs.sock" ] ++ lib.optional (fromCfg != null) fromCfg;
+      socketConfig = {
+        # We also include "%t/ipfs.sock" because there is no way to put the "%t"
+        # in the multiaddr.
+        ListenStream =
+          [ "" "%t/ipfs.sock" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.API);
+        SocketMode = "0660";
+        SocketUser = cfg.user;
+        SocketGroup = cfg.group;
+      };
     };
   };
 
diff --git a/nixos/modules/services/network-filesystems/litestream/litestream.xml b/nixos/modules/services/network-filesystems/litestream/default.md
index 8f5597bb689..8d8486507b7 100644
--- a/nixos/modules/services/network-filesystems/litestream/litestream.xml
+++ b/nixos/modules/services/network-filesystems/litestream/default.md
@@ -1,23 +1,14 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-litestream">
- <title>Litestream</title>
- <para>
-  <link xlink:href="https://litestream.io/">Litestream</link> is a standalone streaming
-  replication tool for SQLite.
- </para>
+# Litestream {#module-services-litestream}
 
- <section xml:id="module-services-litestream-configuration">
-  <title>Configuration</title>
+[Litestream](https://litestream.io/) is a standalone streaming
+replication tool for SQLite.
 
-  <para>
-   Litestream service is managed by a dedicated user named <literal>litestream</literal>
-   which needs permission to the database file. Here's an example config which gives
-   required permissions to access <link linkend="opt-services.grafana.settings.database.path">
-   grafana database</link>:
-<programlisting>
+## Configuration {#module-services-litestream-configuration}
+
+Litestream service is managed by a dedicated user named `litestream`
+which needs permission to the database file. Here's an example config which gives
+required permissions to access [grafana database](#opt-services.grafana.settings.database.path):
+```
 { pkgs, ... }:
 {
   users.users.litestream.extraGroups = [ "grafana" ];
@@ -58,8 +49,4 @@
     };
   };
 }
-</programlisting>
-  </para>
- </section>
-
-</chapter>
+```
diff --git a/nixos/modules/services/network-filesystems/litestream/default.nix b/nixos/modules/services/network-filesystems/litestream/default.nix
index 884ffa50e7c..6e2ec1ccaa3 100644
--- a/nixos/modules/services/network-filesystems/litestream/default.nix
+++ b/nixos/modules/services/network-filesystems/litestream/default.nix
@@ -94,5 +94,6 @@ in
     };
     users.groups.litestream = {};
   };
-  meta.doc = ./litestream.xml;
+
+  meta.doc = ./default.md;
 }
diff --git a/nixos/modules/services/network-filesystems/moosefs.nix b/nixos/modules/services/network-filesystems/moosefs.nix
index ab82a2a07dd..49cbc89d5a9 100644
--- a/nixos/modules/services/network-filesystems/moosefs.nix
+++ b/nixos/modules/services/network-filesystems/moosefs.nix
@@ -85,7 +85,7 @@ in {
         description = lib.mdDoc "Run daemons as user moosefs instead of root.";
       };
 
-      client.enable = mkEnableOption (lib.mdDoc "Moosefs client.");
+      client.enable = mkEnableOption (lib.mdDoc "Moosefs client");
 
       master = {
         enable = mkOption {
@@ -131,7 +131,7 @@ in {
       };
 
       metalogger = {
-        enable = mkEnableOption (lib.mdDoc "Moosefs metalogger daemon.");
+        enable = mkEnableOption (lib.mdDoc "Moosefs metalogger daemon");
 
         settings = mkOption {
           type = types.submodule {
@@ -149,7 +149,7 @@ in {
       };
 
       chunkserver = {
-        enable = mkEnableOption (lib.mdDoc "Moosefs chunkserver daemon.");
+        enable = mkEnableOption (lib.mdDoc "Moosefs chunkserver daemon");
 
         openFirewall = mkOption {
           type = types.bool;
diff --git a/nixos/modules/services/network-filesystems/openafs/lib.nix b/nixos/modules/services/network-filesystems/openafs/lib.nix
index 80628f4dfaf..e5e147a8dc3 100644
--- a/nixos/modules/services/network-filesystems/openafs/lib.nix
+++ b/nixos/modules/services/network-filesystems/openafs/lib.nix
@@ -1,13 +1,13 @@
 { config, lib, ...}:
 
 let
-  inherit (lib) concatStringsSep mkOption types;
+  inherit (lib) concatStringsSep mkOption types optionalString;
 
 in {
 
   mkCellServDB = cellName: db: ''
     >${cellName}
-  '' + (concatStringsSep "\n" (map (dbm: if (dbm.ip != "" && dbm.dnsname != "") then dbm.ip + " #" + dbm.dnsname else "")
+  '' + (concatStringsSep "\n" (map (dbm: optionalString (dbm.ip != "" && dbm.dnsname != "") "${dbm.ip} #${dbm.dnsname}")
                                    db))
      + "\n";
 
diff --git a/nixos/modules/services/network-filesystems/openafs/server.nix b/nixos/modules/services/network-filesystems/openafs/server.nix
index 1c615d3bfb6..ad0fd783567 100644
--- a/nixos/modules/services/network-filesystems/openafs/server.nix
+++ b/nixos/modules/services/network-filesystems/openafs/server.nix
@@ -4,7 +4,8 @@
 with import ./lib.nix { inherit config lib pkgs; };
 
 let
-  inherit (lib) concatStringsSep literalExpression mkIf mkOption optionalString types;
+  inherit (lib) concatStringsSep literalExpression mkIf mkOption mkEnableOption
+  optionalString types;
 
   bosConfig = pkgs.writeText "BosConfig" (''
     restrictmode 1
@@ -24,9 +25,15 @@ let
     parm ${openafsSrv}/libexec/openafs/salvageserver ${cfg.roles.fileserver.salvageserverArgs}
     parm ${openafsSrv}/libexec/openafs/dasalvager ${cfg.roles.fileserver.salvagerArgs}
     end
-  '') + (optionalString (cfg.roles.database.enable && cfg.roles.backup.enable) ''
+  '') + (optionalString (cfg.roles.database.enable && cfg.roles.backup.enable && (!cfg.roles.backup.enableFabs)) ''
     bnode simple buserver 1
-    parm ${openafsSrv}/libexec/openafs/buserver ${cfg.roles.backup.buserverArgs} ${optionalString (cfg.roles.backup.cellServDB != []) "-cellservdb /etc/openafs/backup/"}
+    parm ${openafsSrv}/libexec/openafs/buserver ${cfg.roles.backup.buserverArgs} ${optionalString useBuCellServDB "-cellservdb /etc/openafs/backup/"}
+    end
+  '') + (optionalString (cfg.roles.database.enable &&
+                         cfg.roles.backup.enable &&
+                         cfg.roles.backup.enableFabs) ''
+    bnode simple buserver 1
+    parm ${lib.getBin pkgs.fabs}/bin/fabsys server --config ${fabsConfFile} ${cfg.roles.backup.fabsArgs}
     end
   ''));
 
@@ -34,12 +41,27 @@ let
     pkgs.writeText "NetInfo" ((concatStringsSep "\nf " cfg.advertisedAddresses) + "\n")
   else null;
 
-  buCellServDB = pkgs.writeText "backup-cellServDB-${cfg.cellName}" (mkCellServDB cfg.cellName cfg.roles.backup.cellServDB);
+  buCellServDB = pkgs.writeText "backup-cellServDB-${cfg.cellName}"
+    (mkCellServDB cfg.cellName cfg.roles.backup.cellServDB);
+
+  useBuCellServDB = (cfg.roles.backup.cellServDB != []) && (!cfg.roles.backup.enableFabs);
 
   cfg = config.services.openafsServer;
 
   udpSizeStr = toString cfg.udpPacketSize;
 
+  fabsConfFile = pkgs.writeText "fabs.yaml" (builtins.toJSON ({
+    afs = {
+      aklog = cfg.package + "/bin/aklog";
+      cell = cfg.cellName;
+      dumpscan = cfg.package + "/bin/afsdump_scan";
+      fs = cfg.package + "/bin/fs";
+      pts = cfg.package + "/bin/pts";
+      vos = cfg.package + "/bin/vos";
+    };
+    k5start.command = (lib.getBin pkgs.kstart) + "/bin/k5start";
+  } // cfg.roles.backup.fabsExtraConfig));
+
 in {
 
   options = {
@@ -80,8 +102,8 @@ in {
       };
 
       package = mkOption {
-        default = pkgs.openafs.server or pkgs.openafs;
-        defaultText = literalExpression "pkgs.openafs.server or pkgs.openafs";
+        default = pkgs.openafs;
+        defaultText = literalExpression "pkgs.openafs";
         type = types.package;
         description = lib.mdDoc "OpenAFS package for the server binaries";
       };
@@ -154,16 +176,20 @@ in {
         };
 
         backup = {
-          enable = mkOption {
-            default = false;
-            type = types.bool;
-            description = lib.mdDoc ''
-              Backup server role. Use in conjunction with the
-              `database` role to maintain the Backup
-              Database. Normally only used in conjunction with tape storage
-              or IBM's Tivoli Storage Manager.
-            '';
-          };
+          enable = mkEnableOption (lib.mdDoc ''
+            Backup server role. When using OpenAFS built-in buserver, use in conjunction with the
+            `database` role to maintain the Backup
+            Database. Normally only used in conjunction with tape storage
+            or IBM's Tivoli Storage Manager.
+
+            For a modern backup server, enable this role and see
+            {option}`enableFabs`.
+          '');
+
+          enableFabs = mkEnableOption (lib.mdDoc ''
+            FABS, the flexible AFS backup system. It stores volumes as dump files, relying on other
+            pre-existing backup solutions for handling them.
+          '');
 
           buserverArgs = mkOption {
             default = "";
@@ -181,6 +207,30 @@ in {
               other database server machines.
             '';
           };
+
+          fabsArgs = mkOption {
+            default = "";
+            type = types.str;
+            description = lib.mdDoc ''
+              Arguments to the fabsys process. See
+              {manpage}`fabsys_server(1)` and
+              {manpage}`fabsys_config(1)`.
+            '';
+          };
+
+          fabsExtraConfig = mkOption {
+            default = {};
+            type = types.attrs;
+            description = lib.mdDoc ''
+              Additional configuration parameters for the FABS backup server.
+            '';
+            example = literalExpression ''
+            {
+              afs.localauth = true;
+              afs.keytab = config.sops.secrets.fabsKeytab.path;
+            }
+            '';
+          };
         };
       };
 
@@ -239,7 +289,7 @@ in {
         mode = "0644";
       };
       buCellServDB = {
-        enable = (cfg.roles.backup.cellServDB != []);
+        enable = useBuCellServDB;
         text = mkCellServDB cfg.cellName cfg.roles.backup.cellServDB;
         target = "openafs/backup/CellServDB";
       };
@@ -257,7 +307,7 @@ in {
         preStart = ''
           mkdir -m 0755 -p /var/openafs
           ${optionalString (netInfo != null) "cp ${netInfo} /var/openafs/netInfo"}
-          ${optionalString (cfg.roles.backup.cellServDB != []) "cp ${buCellServDB}"}
+          ${optionalString useBuCellServDB "cp ${buCellServDB}"}
         '';
         serviceConfig = {
           ExecStart = "${openafsBin}/bin/bosserver -nofork";
diff --git a/nixos/modules/services/network-filesystems/webdav-server-rs.nix b/nixos/modules/services/network-filesystems/webdav-server-rs.nix
index 9ea30411181..34e717025e6 100644
--- a/nixos/modules/services/network-filesystems/webdav-server-rs.nix
+++ b/nixos/modules/services/network-filesystems/webdav-server-rs.nix
@@ -28,6 +28,12 @@ in
         description = lib.mdDoc "Group to run under when setuid is not enabled.";
       };
 
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc "Enable debug mode.";
+      };
+
       settings = mkOption {
         type = format.type;
         default = { };
@@ -111,7 +117,7 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
-        ExecStart = "${pkgs.webdav-server-rs}/bin/webdav-server -c ${cfg.configFile}";
+        ExecStart = "${pkgs.webdav-server-rs}/bin/webdav-server ${lib.optionalString cfg.debug "--debug"} -c ${cfg.configFile}";
 
         CapabilityBoundingSet = [
           "CAP_SETUID"
diff --git a/nixos/modules/services/networking/acme-dns.nix b/nixos/modules/services/networking/acme-dns.nix
new file mode 100644
index 00000000000..5c53fa2cc4f
--- /dev/null
+++ b/nixos/modules/services/networking/acme-dns.nix
@@ -0,0 +1,154 @@
+{ lib
+, config
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.services.acme-dns;
+  format = pkgs.formats.toml { };
+  inherit (lib)
+    literalExpression
+    mdDoc
+    mkEnableOption
+    mkOption
+    mkPackageOptionMD
+    types
+    ;
+  domain = "acme-dns.example.com";
+in
+{
+  options.services.acme-dns = {
+    enable = mkEnableOption (mdDoc "acme-dns");
+
+    package = mkPackageOptionMD pkgs "acme-dns" { };
+
+    settings = mkOption {
+      description = mdDoc ''
+        Free-form settings written directly to the `acme-dns.cfg` file.
+        Refer to <https://github.com/joohoi/acme-dns/blob/master/README.md#configuration> for supported values.
+      '';
+
+      default = { };
+
+      type = types.submodule {
+        freeformType = format.type;
+        options = {
+          general = {
+            listen = mkOption {
+              type = types.str;
+              description = mdDoc "IP+port combination to bind and serve the DNS server on.";
+              default = "[::]:53";
+              example = "127.0.0.1:53";
+            };
+
+            protocol = mkOption {
+              type = types.enum [ "both" "both4" "both6" "udp" "udp4" "udp6" "tcp" "tcp4" "tcp6" ];
+              description = mdDoc "Protocols to serve DNS responses on.";
+              default = "both";
+            };
+
+            domain = mkOption {
+              type = types.str;
+              description = mdDoc "Domain name to serve the requests off of.";
+              example = domain;
+            };
+
+            nsname = mkOption {
+              type = types.str;
+              description = mdDoc "Zone name server.";
+              example = domain;
+            };
+
+            nsadmin = mkOption {
+              type = types.str;
+              description = mdDoc "Zone admin email address for `SOA`.";
+              example = "admin.example.com";
+            };
+
+            records = mkOption {
+              type = types.listOf types.str;
+              description = mdDoc "Predefined DNS records served in addition to the `_acme-challenge` TXT records.";
+              example = literalExpression ''
+                [
+                  # replace with your acme-dns server's public IPv4
+                  "${domain}. A 198.51.100.1"
+                  # replace with your acme-dns server's public IPv6
+                  "${domain}. AAAA 2001:db8::1"
+                  # ${domain} should resolve any *.${domain} records
+                  "${domain}. NS ${domain}."
+                ]
+              '';
+            };
+          };
+
+          database = {
+            engine = mkOption {
+              type = types.enum [ "sqlite3" "postgres" ];
+              description = mdDoc "Database engine to use.";
+              default = "sqlite3";
+            };
+            connection = mkOption {
+              type = types.str;
+              description = mdDoc "Database connection string.";
+              example = "postgres://user:password@localhost/acmedns";
+              default = "/var/lib/acme-dns/acme-dns.db";
+            };
+          };
+
+          api = {
+            ip = mkOption {
+              type = types.str;
+              description = mdDoc "IP to bind the HTTP API on.";
+              default = "[::]";
+              example = "127.0.0.1";
+            };
+
+            port = mkOption {
+              type = types.port;
+              description = mdDoc "Listen port for the HTTP API.";
+              default = 8080;
+              # acme-dns expects this value to be a string
+              apply = toString;
+            };
+
+            disable_registration = mkOption {
+              type = types.bool;
+              description = mdDoc "Whether to disable the HTTP registration endpoint.";
+              default = false;
+              example = true;
+            };
+
+            tls = mkOption {
+              type = types.enum [ "letsencrypt" "letsencryptstaging" "cert" "none" ];
+              description = mdDoc "TLS backend to use.";
+              default = "none";
+            };
+          };
+
+
+          logconfig = {
+            loglevel = mkOption {
+              type = types.enum [ "error" "warning" "info" "debug" ];
+              description = mdDoc "Level to log on.";
+              default = "info";
+            };
+          };
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.packages = [ cfg.package ];
+    systemd.services.acme-dns = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = [ "" "${lib.getExe cfg.package} -c ${format.generate "acme-dns.toml" cfg.settings}" ];
+        StateDirectory = "acme-dns";
+        WorkingDirectory = "%S/acme-dns";
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/adguardhome.nix b/nixos/modules/services/networking/adguardhome.nix
index bda99cb7942..1701e5b439c 100644
--- a/nixos/modules/services/networking/adguardhome.nix
+++ b/nixos/modules/services/networking/adguardhome.nix
@@ -41,6 +41,20 @@ in
       '';
     };
 
+    allowDHCP = mkOption {
+      default = cfg.settings.dhcp.enabled or false;
+      defaultText = literalExpression ''config.services.adguardhome.settings.dhcp.enabled or false'';
+      type = bool;
+      description = lib.mdDoc ''
+        Allows AdGuard Home to open raw sockets (`CAP_NET_RAW`), which is
+        required for the integrated DHCP server.
+
+        The default enables this conditionally if the declarative configuration
+        enables the integrated DHCP server. Manually setting this option is only
+        required for non-declarative setups.
+      '';
+    };
+
     mutableSettings = mkOption {
       default = true;
       type = bool;
@@ -147,7 +161,7 @@ in
       serviceConfig = {
         DynamicUser = true;
         ExecStart = "${pkgs.adguardhome}/bin/adguardhome ${args}";
-        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ] ++ optionals cfg.allowDHCP [ "CAP_NET_RAW" ];
         Restart = "always";
         RestartSec = 10;
         RuntimeDirectory = "AdGuardHome";
diff --git a/nixos/modules/services/networking/alice-lg.nix b/nixos/modules/services/networking/alice-lg.nix
new file mode 100644
index 00000000000..06b9ac89f12
--- /dev/null
+++ b/nixos/modules/services/networking/alice-lg.nix
@@ -0,0 +1,101 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.alice-lg;
+  settingsFormat = pkgs.formats.ini { };
+in
+{
+  options = {
+    services.alice-lg = {
+      enable = mkEnableOption (lib.mdDoc "Alice Looking Glass");
+
+      package = mkPackageOptionMD pkgs "alice-lg" { };
+
+      settings = mkOption {
+        type = settingsFormat.type;
+        default = { };
+        description = lib.mdDoc ''
+          alice-lg configuration, for configuration options see the example on [github](https://github.com/alice-lg/alice-lg/blob/main/etc/alice-lg/alice.example.conf)
+        '';
+        example = literalExpression ''
+          {
+            server = {
+              # configures the built-in webserver and provides global application settings
+              listen_http = "127.0.0.1:7340";
+              enable_prefix_lookup = true;
+              asn = 9033;
+              store_backend = postgres;
+              routes_store_refresh_parallelism = 5;
+              neighbors_store_refresh_parallelism = 10000;
+              routes_store_refresh_interval = 5;
+              neighbors_store_refresh_interval = 5;
+            };
+            postgres = {
+              url = "postgres://postgres:postgres@localhost:5432/alice";
+              min_connections = 2;
+              max_connections = 128;
+            };
+            pagination = {
+              routes_filtered_page_size = 250;
+              routes_accepted_page_size = 250;
+              routes_not_exported_page_size = 250;
+            };
+          }
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment = {
+      etc."alice-lg/alice.conf".source = settingsFormat.generate "alice-lg.conf" cfg.settings;
+    };
+    systemd.services = {
+      alice-lg = {
+        wants = [ "network.target" ];
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        description = "Alice Looking Glass";
+        serviceConfig = {
+          DynamicUser = true;
+          Type = "simple";
+          Restart = "on-failure";
+          RestartSec = 15;
+          ExecStart = "${cfg.package}/bin/alice-lg";
+          StateDirectoryMode = "0700";
+          UMask = "0007";
+          CapabilityBoundingSet = "";
+          NoNewPrivileges = true;
+          ProtectSystem = "strict";
+          PrivateTmp = true;
+          PrivateDevices = true;
+          PrivateUsers = true;
+          ProtectHostname = true;
+          ProtectClock = true;
+          ProtectKernelTunables = true;
+          ProtectKernelModules = true;
+          ProtectKernelLogs = true;
+          ProtectControlGroups = true;
+          RestrictAddressFamilies = [ "AF_INET AF_INET6" ];
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          PrivateMounts = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = "~@clock @privileged @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @reboot @setuid @swap";
+          BindReadOnlyPaths = [
+            "-/etc/resolv.conf"
+            "-/etc/nsswitch.conf"
+            "-/etc/ssl/certs"
+            "-/etc/static/ssl/certs"
+            "-/etc/hosts"
+            "-/etc/localtime"
+          ];
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/avahi-daemon.nix b/nixos/modules/services/networking/avahi-daemon.nix
index 3933ed5a231..bdbf9aad9ac 100644
--- a/nixos/modules/services/networking/avahi-daemon.nix
+++ b/nixos/modules/services/networking/avahi-daemon.nix
@@ -5,7 +5,7 @@ with lib;
 let
   cfg = config.services.avahi;
 
-  yesNo = yes : if yes then "yes" else "no";
+  yesNo = yes: if yes then "yes" else "no";
 
   avahiDaemonConf = with cfg; pkgs.writeText "avahi-daemon.conf" ''
     [server]
@@ -17,7 +17,8 @@ let
     browse-domains=${concatStringsSep ", " browseDomains}
     use-ipv4=${yesNo ipv4}
     use-ipv6=${yesNo ipv6}
-    ${optionalString (interfaces!=null) "allow-interfaces=${concatStringsSep "," interfaces}"}
+    ${optionalString (allowInterfaces!=null) "allow-interfaces=${concatStringsSep "," allowInterfaces}"}
+    ${optionalString (denyInterfaces!=null) "deny-interfaces=${concatStringsSep "," denyInterfaces}"}
     ${optionalString (domainName!=null) "domain-name=${domainName}"}
     allow-point-to-point=${yesNo allowPointToPoint}
     ${optionalString (cacheEntriesMax!=null) "cache-entries-max=${toString cacheEntriesMax}"}
@@ -39,6 +40,10 @@ let
   '';
 in
 {
+  imports = [
+    (lib.mkRenamedOptionModule [ "services" "avahi" "interfaces" ] [ "services" "avahi" "allowInterfaces" ])
+  ];
+
   options.services.avahi = {
     enable = mkOption {
       type = types.bool;
@@ -47,7 +52,16 @@ in
         Whether to run the Avahi daemon, which allows Avahi clients
         to use Avahi's service discovery facilities and also allows
         the local machine to advertise its presence and services
-        (through the mDNS responder implemented by `avahi-daemon').
+        (through the mDNS responder implemented by `avahi-daemon`).
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.avahi;
+      defaultText = literalExpression "pkgs.avahi";
+      description = lib.mdDoc ''
+        The avahi package to use for running the daemon.
       '';
     };
 
@@ -91,7 +105,7 @@ in
       description = lib.mdDoc "Whether to use IPv6.";
     };
 
-    interfaces = mkOption {
+    allowInterfaces = mkOption {
       type = types.nullOr (types.listOf types.str);
       default = null;
       description = lib.mdDoc ''
@@ -101,6 +115,17 @@ in
       '';
     };
 
+    denyInterfaces = mkOption {
+      type = types.nullOr (types.listOf types.str);
+      default = null;
+      description = lib.mdDoc ''
+        List of network interfaces that should be ignored by the
+        {command}`avahi-daemon`. Other unspecified interfaces will be used,
+        unless {option}`allowInterfaces` is set. This option takes precedence
+        over {option}`allowInterfaces`.
+      '';
+    };
+
     openFirewall = mkOption {
       type = types.bool;
       default = true;
@@ -134,7 +159,7 @@ in
 
     extraServiceFiles = mkOption {
       type = with types; attrsOf (either str path);
-      default = {};
+      default = { };
       example = literalExpression ''
         {
           ssh = "''${pkgs.avahi}/etc/avahi/services/ssh.service";
@@ -205,7 +230,7 @@ in
       default = false;
       description = lib.mdDoc ''
         Whether to enable the mDNS NSS (Name Service Switch) plug-in.
-        Enabling it allows applications to resolve names in the `.local'
+        Enabling it allows applications to resolve names in the `.local`
         domain by transparently querying the Avahi daemon.
       '';
     };
@@ -236,7 +261,7 @@ in
       isSystemUser = true;
     };
 
-    users.groups.avahi = {};
+    users.groups.avahi = { };
 
     system.nssModules = optional cfg.nssmdns pkgs.nssmdns;
     system.nssDatabases.hosts = optionals cfg.nssmdns (mkMerge [
@@ -244,12 +269,14 @@ in
       (mkAfter [ "mdns" ]) # after dns
     ]);
 
-    environment.systemPackages = [ pkgs.avahi ];
+    environment.systemPackages = [ cfg.package ];
 
-    environment.etc = (mapAttrs' (n: v: nameValuePair
-      "avahi/services/${n}.service"
-      { ${if types.path.check v then "source" else "text"} = v; }
-    ) cfg.extraServiceFiles);
+    environment.etc = (mapAttrs'
+      (n: v: nameValuePair
+        "avahi/services/${n}.service"
+        { ${if types.path.check v then "source" else "text"} = v; }
+      )
+      cfg.extraServiceFiles);
 
     systemd.sockets.avahi-daemon = {
       description = "Avahi mDNS/DNS-SD Stack Activation Socket";
@@ -268,18 +295,19 @@ in
       # return a sensible value.
       environment.LD_LIBRARY_PATH = config.system.nssModules.path;
 
-      path = [ pkgs.coreutils pkgs.avahi ];
+      path = [ pkgs.coreutils cfg.package ];
 
       serviceConfig = {
         NotifyAccess = "main";
         BusName = "org.freedesktop.Avahi";
         Type = "dbus";
-        ExecStart = "${pkgs.avahi}/sbin/avahi-daemon --syslog -f ${avahiDaemonConf}";
+        ExecStart = "${cfg.package}/sbin/avahi-daemon --syslog -f ${avahiDaemonConf}";
+        ConfigurationDirectory = "avahi/services";
       };
     };
 
     services.dbus.enable = true;
-    services.dbus.packages = [ pkgs.avahi ];
+    services.dbus.packages = [ cfg.package ];
 
     networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ 5353 ];
   };
diff --git a/nixos/modules/services/networking/bind.nix b/nixos/modules/services/networking/bind.nix
index f963e341546..f1829747bb1 100644
--- a/nixos/modules/services/networking/bind.nix
+++ b/nixos/modules/services/networking/bind.nix
@@ -36,6 +36,17 @@ let
         description = lib.mdDoc "Addresses who may request zone transfers.";
         default = [ ];
       };
+      allowQuery = mkOption {
+        type = types.listOf types.str;
+        description = lib.mdDoc ''
+          List of address ranges allowed to query this zone. Instead of the address(es), this may instead
+          contain the single string "any".
+
+          NOTE: This overrides the global-level `allow-query` setting, which is set to the contents
+          of `cachenetworks`.
+        '';
+        default = [ "any" ];
+      };
       extraConfig = mkOption {
         type = types.str;
         description = lib.mdDoc "Extra zone config to be appended at the end of the zone section.";
@@ -69,7 +80,7 @@ let
       ${cfg.extraConfig}
 
       ${ concatMapStrings
-          ({ name, file, master ? true, slaves ? [], masters ? [], extraConfig ? "" }:
+          ({ name, file, master ? true, slaves ? [], masters ? [], allowQuery ? [], extraConfig ? "" }:
             ''
               zone "${name}" {
                 type ${if master then "master" else "slave"};
@@ -87,7 +98,7 @@ let
                      };
                    ''
                 }
-                allow-query { any; };
+                allow-query { ${concatMapStrings (ip: "${ip}; ") allowQuery}};
                 ${extraConfig}
               };
             '')
@@ -120,7 +131,9 @@ in
         description = lib.mdDoc ''
           What networks are allowed to use us as a resolver.  Note
           that this is for recursive queries -- all networks are
-          allowed to query zones configured with the `zones` option.
+          allowed to query zones configured with the `zones` option
+          by default (although this may be overridden within each
+          zone's configuration, via the `allowQuery` option).
           It is recommended that you limit cacheNetworks to avoid your
           server being used for DNS amplification attacks.
         '';
diff --git a/nixos/modules/services/networking/bird-lg.nix b/nixos/modules/services/networking/bird-lg.nix
index 11cfe3e7ec0..dc861dbfd11 100644
--- a/nixos/modules/services/networking/bird-lg.nix
+++ b/nixos/modules/services/networking/bird-lg.nix
@@ -4,6 +4,49 @@ with lib;
 
 let
   cfg = config.services.bird-lg;
+
+  stringOrConcat = sep: v: if builtins.isString v then v else concatStringsSep sep v;
+
+  frontend_args = let
+    fe = cfg.frontend;
+  in {
+    "--servers" = concatStringsSep "," fe.servers;
+    "--domain" = fe.domain;
+    "--listen" = fe.listenAddress;
+    "--proxy-port" = fe.proxyPort;
+    "--whois" = fe.whois;
+    "--dns-interface" = fe.dnsInterface;
+    "--bgpmap-info" = concatStringsSep "," cfg.frontend.bgpMapInfo;
+    "--title-brand" = fe.titleBrand;
+    "--navbar-brand" = fe.navbar.brand;
+    "--navbar-brand-url" = fe.navbar.brandURL;
+    "--navbar-all-servers" = fe.navbar.allServers;
+    "--navbar-all-url" = fe.navbar.allServersURL;
+    "--net-specific-mode" = fe.netSpecificMode;
+    "--protocol-filter" = concatStringsSep "," cfg.frontend.protocolFilter;
+  };
+
+  proxy_args = let
+    px = cfg.proxy;
+  in {
+    "--allowed" = concatStringsSep "," px.allowedIPs;
+    "--bird" = px.birdSocket;
+    "--listen" = px.listenAddress;
+    "--traceroute_bin" = px.traceroute.binary;
+    "--traceroute_flags" = concatStringsSep " " px.traceroute.flags;
+    "--traceroute_raw" = px.traceroute.rawOutput;
+  };
+
+  mkArgValue = value:
+    if isString value
+      then escapeShellArg value
+      else if isBool value
+        then boolToString value
+        else toString value;
+
+  filterNull = filterAttrs (_: v: v != "" && v != null && v != []);
+
+  argsAttrToList = args: mapAttrsToList (name: value: "${name} " + mkArgValue value ) (filterNull args);
 in
 {
   options = {
@@ -44,14 +87,12 @@ in
 
         domain = mkOption {
           type = types.str;
-          default = "";
           example = "dn42.lantian.pub";
           description = lib.mdDoc "Server name domain suffixes.";
         };
 
         servers = mkOption {
           type = types.listOf types.str;
-          default = [ ];
           example = [ "gigsgigscloud" "hostdare" ];
           description = lib.mdDoc "Server name prefixes.";
         };
@@ -134,10 +175,14 @@ in
         };
 
         extraArgs = mkOption {
-          type = types.lines;
-          default = "";
+          type = with types; either lines (listOf str);
+          default = [ ];
           description = lib.mdDoc ''
             Extra parameters documented [here](https://github.com/xddxdd/bird-lg-go#frontend).
+
+            :::{.note}
+            Passing lines (plain strings) is deprecated in favour of passing lists of strings.
+            :::
           '';
         };
       };
@@ -160,8 +205,7 @@ in
 
         birdSocket = mkOption {
           type = types.str;
-          default = "/run/bird.ctl";
-          example = "/var/run/bird/bird.ctl";
+          default = "/var/run/bird/bird.ctl";
           description = lib.mdDoc "Bird control socket path.";
         };
 
@@ -173,6 +217,12 @@ in
             description = lib.mdDoc "Traceroute's binary path.";
           };
 
+          flags = mkOption {
+            type = with types; listOf str;
+            default = [ ];
+            description = lib.mdDoc "Flags for traceroute process";
+          };
+
           rawOutput = mkOption {
             type = types.bool;
             default = false;
@@ -181,10 +231,14 @@ in
         };
 
         extraArgs = mkOption {
-          type = types.lines;
-          default = "";
+          type = with types; either lines (listOf str);
+          default = [ ];
           description = lib.mdDoc ''
             Extra parameters documented [here](https://github.com/xddxdd/bird-lg-go#proxy).
+
+            :::{.note}
+            Passing lines (plain strings) is deprecated in favour of passing lists of strings.
+            :::
           '';
         };
       };
@@ -194,6 +248,16 @@ in
   ###### implementation
 
   config = {
+
+    warnings =
+      lib.optional (cfg.frontend.enable  && builtins.isString cfg.frontend.extraArgs) ''
+        Passing strings to `services.bird-lg.frontend.extraOptions' is deprecated. Please pass a list of strings instead.
+      ''
+      ++ lib.optional (cfg.proxy.enable  && builtins.isString cfg.proxy.extraArgs) ''
+        Passing strings to `services.bird-lg.proxy.extraOptions' is deprecated. Please pass a list of strings instead.
+      ''
+    ;
+
     systemd.services = {
       bird-lg-frontend = mkIf cfg.frontend.enable {
         enable = true;
@@ -211,23 +275,8 @@ in
         };
         script = ''
           ${cfg.package}/bin/frontend \
-            --servers ${concatStringsSep "," cfg.frontend.servers } \
-            --domain ${cfg.frontend.domain} \
-            --listen ${cfg.frontend.listenAddress} \
-            --proxy-port ${toString cfg.frontend.proxyPort} \
-            --whois ${cfg.frontend.whois} \
-            --dns-interface ${cfg.frontend.dnsInterface} \
-            --bgpmap-info ${concatStringsSep "," cfg.frontend.bgpMapInfo } \
-            --title-brand ${cfg.frontend.titleBrand} \
-            --navbar-brand ${cfg.frontend.navbar.brand} \
-            --navbar-brand-url ${cfg.frontend.navbar.brandURL} \
-            --navbar-all-servers ${cfg.frontend.navbar.allServers} \
-            --navbar-all-url ${cfg.frontend.navbar.allServersURL} \
-            --net-specific-mode ${cfg.frontend.netSpecificMode} \
-            --protocol-filter ${concatStringsSep "," cfg.frontend.protocolFilter } \
-            --name-filter ${cfg.frontend.nameFilter} \
-            --time-out ${toString cfg.frontend.timeout} \
-            ${cfg.frontend.extraArgs}
+            ${concatStringsSep " \\\n  " (argsAttrToList frontend_args)} \
+            ${stringOrConcat " " cfg.frontend.extraArgs}
         '';
       };
 
@@ -247,12 +296,8 @@ in
         };
         script = ''
           ${cfg.package}/bin/proxy \
-          --allowed ${concatStringsSep "," cfg.proxy.allowedIPs } \
-          --bird ${cfg.proxy.birdSocket} \
-          --listen ${cfg.proxy.listenAddress} \
-          --traceroute_bin ${cfg.proxy.traceroute.binary}
-          --traceroute_raw ${boolToString cfg.proxy.traceroute.rawOutput}
-          ${cfg.proxy.extraArgs}
+            ${concatStringsSep " \\\n  " (argsAttrToList proxy_args)} \
+            ${stringOrConcat " " cfg.proxy.extraArgs}
         '';
       };
     };
@@ -266,4 +311,9 @@ in
       };
     };
   };
+
+  meta.maintainers = with lib.maintainers; [
+    e1mo
+    tchekda
+  ];
 }
diff --git a/nixos/modules/services/networking/birdwatcher.nix b/nixos/modules/services/networking/birdwatcher.nix
new file mode 100644
index 00000000000..a129b7a2b4c
--- /dev/null
+++ b/nixos/modules/services/networking/birdwatcher.nix
@@ -0,0 +1,129 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.birdwatcher;
+in
+{
+  options = {
+    services.birdwatcher = {
+      package = mkOption {
+        type = types.package;
+        default = pkgs.birdwatcher;
+        defaultText = literalExpression "pkgs.birdwatcher";
+        description = lib.mdDoc "The Birdwatcher package to use.";
+      };
+      enable = mkEnableOption (lib.mdDoc "Birdwatcher");
+      flags = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        example = [ "-worker-pool-size 16" "-6" ];
+        description = lib.mdDoc ''
+          Flags to append to the program call
+        '';
+      };
+
+      settings = mkOption {
+        type = types.lines;
+        default = { };
+        description = lib.mdDoc ''
+          birdwatcher configuration, for configuration options see the example on [github](https://github.com/alice-lg/birdwatcher/blob/master/etc/birdwatcher/birdwatcher.conf)
+        '';
+        example = literalExpression ''
+          [server]
+          allow_from = []
+          allow_uncached = false
+          modules_enabled = ["status",
+                             "protocols",
+                             "protocols_bgp",
+                             "protocols_short",
+                             "routes_protocol",
+                             "routes_peer",
+                             "routes_table",
+                             "routes_table_filtered",
+                             "routes_table_peer",
+                             "routes_filtered",
+                             "routes_prefixed",
+                             "routes_noexport",
+                             "routes_pipe_filtered_count",
+                             "routes_pipe_filtered"
+                            ]
+
+          [status]
+          reconfig_timestamp_source = "bird"
+          reconfig_timestamp_match = "# created: (.*)"
+
+          filter_fields = []
+
+          [bird]
+          listen = "0.0.0.0:29184"
+          config = "/etc/bird/bird2.conf"
+          birdc  = "''${pkgs.bird}/bin/birdc"
+          ttl = 5 # time to live (in minutes) for caching of cli output
+
+          [parser]
+          filter_fields = []
+
+          [cache]
+          use_redis = false # if not using redis cache, activate housekeeping to save memory!
+
+          [housekeeping]
+          interval = 5
+          force_release_memory = true
+        '';
+      };
+    };
+  };
+
+  config =
+    let flagsStr = escapeShellArgs cfg.flags;
+    in lib.mkIf cfg.enable {
+      environment.etc."birdwatcher/birdwatcher.conf".source = pkgs.writeTextFile {
+        name = "birdwatcher.conf";
+        text = cfg.settings;
+      };
+      systemd.services = {
+        birdwatcher = {
+          wants = [ "network.target" ];
+          after = [ "network.target" ];
+          wantedBy = [ "multi-user.target" ];
+          description = "Birdwatcher";
+          serviceConfig = {
+            Type = "simple";
+            Restart = "on-failure";
+            RestartSec = 15;
+            ExecStart = "${cfg.package}/bin/birdwatcher";
+            StateDirectoryMode = "0700";
+            UMask = "0117";
+            NoNewPrivileges = true;
+            ProtectSystem = "strict";
+            PrivateTmp = true;
+            PrivateDevices = true;
+            ProtectHostname = true;
+            ProtectClock = true;
+            ProtectKernelTunables = true;
+            ProtectKernelModules = true;
+            ProtectKernelLogs = true;
+            ProtectControlGroups = true;
+            RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ];
+            LockPersonality = true;
+            MemoryDenyWriteExecute = true;
+            RestrictRealtime = true;
+            RestrictSUIDSGID = true;
+            PrivateMounts = true;
+            SystemCallArchitectures = "native";
+            SystemCallFilter = "~@clock @privileged @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @reboot @setuid @swap";
+            BindReadOnlyPaths = [
+              "-/etc/resolv.conf"
+              "-/etc/nsswitch.conf"
+              "-/etc/ssl/certs"
+              "-/etc/static/ssl/certs"
+              "-/etc/hosts"
+              "-/etc/localtime"
+            ];
+          };
+        };
+      };
+    };
+}
diff --git a/nixos/modules/services/networking/blockbook-frontend.nix b/nixos/modules/services/networking/blockbook-frontend.nix
index ab784563e4a..46b26195d21 100644
--- a/nixos/modules/services/networking/blockbook-frontend.nix
+++ b/nixos/modules/services/networking/blockbook-frontend.nix
@@ -10,7 +10,7 @@ let
 
     options = {
 
-      enable = mkEnableOption (lib.mdDoc "blockbook-frontend application.");
+      enable = mkEnableOption (lib.mdDoc "blockbook-frontend application");
 
       package = mkOption {
         type = types.package;
diff --git a/nixos/modules/services/networking/blocky.nix b/nixos/modules/services/networking/blocky.nix
index 97144854561..30a41fa6a42 100644
--- a/nixos/modules/services/networking/blocky.nix
+++ b/nixos/modules/services/networking/blocky.nix
@@ -31,6 +31,7 @@ in
       serviceConfig = {
         DynamicUser = true;
         ExecStart = "${pkgs.blocky}/bin/blocky --config ${configFile}";
+        Restart = "on-failure";
 
         AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
         CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
diff --git a/nixos/modules/services/networking/cgit.nix b/nixos/modules/services/networking/cgit.nix
new file mode 100644
index 00000000000..672b0b030ee
--- /dev/null
+++ b/nixos/modules/services/networking/cgit.nix
@@ -0,0 +1,203 @@
+{ config, lib, pkgs, ...}:
+
+with lib;
+
+let
+  cfgs = config.services.cgit;
+
+  settingType = with types; oneOf [ bool int str ];
+
+  genAttrs' = names: f: listToAttrs (map f names);
+
+  regexEscape =
+    let
+      # taken from https://github.com/python/cpython/blob/05cb728d68a278d11466f9a6c8258d914135c96c/Lib/re.py#L251-L266
+      special = [
+        "(" ")" "[" "]" "{" "}" "?" "*" "+" "-" "|" "^" "$" "\\" "." "&" "~"
+        "#" " " "\t" "\n" "\r" "\v" "\f"
+      ];
+    in
+      replaceStrings special (map (c: "\\${c}") special);
+
+  stripLocation = cfg: removeSuffix "/" cfg.nginx.location;
+
+  regexLocation = cfg: regexEscape (stripLocation cfg);
+
+  mkFastcgiPass = cfg: ''
+    ${if cfg.nginx.location == "/" then ''
+      fastcgi_param PATH_INFO $uri;
+    '' else ''
+      fastcgi_split_path_info ^(${regexLocation cfg})(/.+)$;
+      fastcgi_param PATH_INFO $fastcgi_path_info;
+    ''
+    }fastcgi_pass unix:${config.services.fcgiwrap.socketAddress};
+  '';
+
+  cgitrcLine = name: value: "${name}=${
+    if value == true then
+      "1"
+    else if value == false then
+      "0"
+    else
+      toString value
+  }";
+
+  mkCgitrc = cfg: pkgs.writeText "cgitrc" ''
+    # global settings
+    ${concatStringsSep "\n" (
+        mapAttrsToList
+          cgitrcLine
+          ({ virtual-root = cfg.nginx.location; } // cfg.settings)
+      )
+    }
+    ${optionalString (cfg.scanPath != null) (cgitrcLine "scan-path" cfg.scanPath)}
+
+    # repository settings
+    ${concatStrings (
+        mapAttrsToList
+          (url: settings: ''
+            ${cgitrcLine "repo.url" url}
+            ${concatStringsSep "\n" (
+                mapAttrsToList (name: cgitrcLine "repo.${name}") settings
+              )
+            }
+          '')
+          cfg.repos
+      )
+    }
+
+    # extra config
+    ${cfg.extraConfig}
+  '';
+
+  mkCgitReposDir = cfg:
+    if cfg.scanPath != null then
+      cfg.scanPath
+    else
+      pkgs.runCommand "cgit-repos" {
+        preferLocalBuild = true;
+        allowSubstitutes = false;
+      } ''
+        mkdir -p "$out"
+        ${
+          concatStrings (
+            mapAttrsToList
+              (name: value: ''
+                ln -s ${escapeShellArg value.path} "$out"/${escapeShellArg name}
+              '')
+              cfg.repos
+          )
+        }
+      '';
+
+in
+{
+  options = {
+    services.cgit = mkOption {
+      description = mdDoc "Configure cgit instances.";
+      default = {};
+      type = types.attrsOf (types.submodule ({ config, ... }: {
+        options = {
+          enable = mkEnableOption (mdDoc "cgit");
+
+          package = mkPackageOptionMD pkgs "cgit" {};
+
+          nginx.virtualHost = mkOption {
+            description = mdDoc "VirtualHost to serve cgit on, defaults to the attribute name.";
+            type = types.str;
+            default = config._module.args.name;
+            example = "git.example.com";
+          };
+
+          nginx.location = mkOption {
+            description = mdDoc "Location to serve cgit under.";
+            type = types.str;
+            default = "/";
+            example = "/git/";
+          };
+
+          repos = mkOption {
+            description = mdDoc "cgit repository settings, see cgitrc(5)";
+            type = with types; attrsOf (attrsOf settingType);
+            default = {};
+            example = {
+              blah = {
+                path = "/var/lib/git/example";
+                desc = "An example repository";
+              };
+            };
+          };
+
+          scanPath = mkOption {
+            description = mdDoc "A path which will be scanned for repositories.";
+            type = types.nullOr types.path;
+            default = null;
+            example = "/var/lib/git";
+          };
+
+          settings = mkOption {
+            description = mdDoc "cgit configuration, see cgitrc(5)";
+            type = types.attrsOf settingType;
+            default = {};
+            example = literalExpression ''
+              {
+                enable-follow-links = true;
+                source-filter = "''${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py";
+              }
+            '';
+          };
+
+          extraConfig = mkOption {
+            description = mdDoc "These lines go to the end of cgitrc verbatim.";
+            type = types.lines;
+            default = "";
+          };
+        };
+      }));
+    };
+  };
+
+  config = mkIf (any (cfg: cfg.enable) (attrValues cfgs)) {
+    assertions = mapAttrsToList (vhost: cfg: {
+      assertion = !cfg.enable || (cfg.scanPath == null) != (cfg.repos == {});
+      message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set.";
+    }) cfgs;
+
+    services.fcgiwrap.enable = true;
+
+    services.nginx.enable = true;
+
+    services.nginx.virtualHosts = mkMerge (mapAttrsToList (_: cfg: {
+      ${cfg.nginx.virtualHost} = {
+        locations = (
+          genAttrs'
+            [ "cgit.css" "cgit.png" "favicon.ico" "robots.txt" ]
+            (name: nameValuePair "= ${stripLocation cfg}/${name}" {
+              extraConfig = ''
+                alias ${cfg.package}/cgit/${name};
+              '';
+            })
+        ) // {
+          "~ ${regexLocation cfg}/.+/(info/refs|git-upload-pack)" = {
+            fastcgiParams = rec {
+              SCRIPT_FILENAME = "${pkgs.git}/libexec/git-core/git-http-backend";
+              GIT_HTTP_EXPORT_ALL = "1";
+              GIT_PROJECT_ROOT = mkCgitReposDir cfg;
+              HOME = GIT_PROJECT_ROOT;
+            };
+            extraConfig = mkFastcgiPass cfg;
+          };
+          "${stripLocation cfg}/" = {
+            fastcgiParams = {
+              SCRIPT_FILENAME = "${cfg.package}/cgit/cgit.cgi";
+              QUERY_STRING = "$args";
+              HTTP_HOST = "$server_name";
+              CGIT_CONFIG = mkCgitrc cfg;
+            };
+            extraConfig = mkFastcgiPass cfg;
+          };
+        };
+      };
+    }) cfgs);
+  };
+}
diff --git a/nixos/modules/services/networking/consul.nix b/nixos/modules/services/networking/consul.nix
index f1c36138be3..955463b9031 100644
--- a/nixos/modules/services/networking/consul.nix
+++ b/nixos/modules/services/networking/consul.nix
@@ -199,7 +199,7 @@ in
             (filterAttrs (n: _: hasPrefix "consul.d/" n) config.environment.etc);
 
         serviceConfig = {
-          ExecStart = "@${cfg.package}/bin/consul consul agent -config-dir /etc/consul.d"
+          ExecStart = "@${lib.getExe cfg.package} consul agent -config-dir /etc/consul.d"
             + concatMapStrings (n: " -config-file ${n}") configFiles;
           ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
           PermissionsStartOnly = true;
@@ -207,10 +207,10 @@ in
           Restart = "on-failure";
           TimeoutStartSec = "infinity";
         } // (optionalAttrs (cfg.leaveOnStop) {
-          ExecStop = "${cfg.package}/bin/consul leave";
+          ExecStop = "${lib.getExe cfg.package} leave";
         });
 
-        path = with pkgs; [ iproute2 gnugrep gawk consul ];
+        path = with pkgs; [ iproute2 gawk cfg.package ];
         preStart = let
           family = if cfg.forceAddrFamily == "ipv6" then
             "-6"
@@ -269,7 +269,7 @@ in
 
         serviceConfig = {
           ExecStart = ''
-            ${cfg.alerts.package}/bin/consul-alerts start \
+            ${lib.getExe cfg.alerts.package} start \
               --alert-addr=${cfg.alerts.listenAddr} \
               --consul-addr=${cfg.alerts.consulAddr} \
               ${optionalString cfg.alerts.watchChecks "--watch-checks"} \
diff --git a/nixos/modules/services/networking/ddclient.nix b/nixos/modules/services/networking/ddclient.nix
deleted file mode 100644
index 4d843641f58..00000000000
--- a/nixos/modules/services/networking/ddclient.nix
+++ /dev/null
@@ -1,242 +0,0 @@
-{ config, pkgs, lib, ... }:
-
-let
-  cfg = config.services.ddclient;
-  boolToStr = bool: if bool then "yes" else "no";
-  dataDir = "/var/lib/ddclient";
-  StateDirectory = builtins.baseNameOf dataDir;
-  RuntimeDirectory = StateDirectory;
-
-  configFile' = pkgs.writeText "ddclient.conf" ''
-    # This file can be used as a template for configFile or is automatically generated by Nix options.
-    cache=${dataDir}/ddclient.cache
-    foreground=YES
-    use=${cfg.use}
-    login=${cfg.username}
-    password=${if cfg.protocol == "nsupdate" then "/run/${RuntimeDirectory}/ddclient.key" else "@password_placeholder@"}
-    protocol=${cfg.protocol}
-    ${lib.optionalString (cfg.script != "") "script=${cfg.script}"}
-    ${lib.optionalString (cfg.server != "") "server=${cfg.server}"}
-    ${lib.optionalString (cfg.zone != "")   "zone=${cfg.zone}"}
-    ssl=${boolToStr cfg.ssl}
-    wildcard=YES
-    ipv6=${boolToStr cfg.ipv6}
-    quiet=${boolToStr cfg.quiet}
-    verbose=${boolToStr cfg.verbose}
-    ${cfg.extraConfig}
-    ${lib.concatStringsSep "," cfg.domains}
-  '';
-  configFile = if (cfg.configFile != null) then cfg.configFile else configFile';
-
-  preStart = ''
-    install ${configFile} /run/${RuntimeDirectory}/ddclient.conf
-    ${lib.optionalString (cfg.configFile == null) (if (cfg.protocol == "nsupdate") then ''
-      install ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key
-    '' else if (cfg.passwordFile != null) then ''
-      "${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "${cfg.passwordFile}" "/run/${RuntimeDirectory}/ddclient.conf"
-    '' else ''
-      sed -i '/^password=@password_placeholder@$/d' /run/${RuntimeDirectory}/ddclient.conf
-    '')}
-  '';
-
-in
-
-with lib;
-
-{
-
-  imports = [
-    (mkChangedOptionModule [ "services" "ddclient" "domain" ] [ "services" "ddclient" "domains" ]
-      (config:
-        let value = getAttrFromPath [ "services" "ddclient" "domain" ] config;
-        in if value != "" then [ value ] else []))
-    (mkRemovedOptionModule [ "services" "ddclient" "homeDir" ] "")
-    (mkRemovedOptionModule [ "services" "ddclient" "password" ] "Use services.ddclient.passwordFile instead.")
-  ];
-
-  ###### interface
-
-  options = {
-
-    services.ddclient = with lib.types; {
-
-      enable = mkOption {
-        default = false;
-        type = bool;
-        description = lib.mdDoc ''
-          Whether to synchronise your machine's IP address with a dynamic DNS provider (e.g. dyndns.org).
-        '';
-      };
-
-      package = mkOption {
-        type = package;
-        default = pkgs.ddclient;
-        defaultText = lib.literalExpression "pkgs.ddclient";
-        description = lib.mdDoc ''
-          The ddclient executable package run by the service.
-        '';
-      };
-
-      domains = mkOption {
-        default = [ "" ];
-        type = listOf str;
-        description = lib.mdDoc ''
-          Domain name(s) to synchronize.
-        '';
-      };
-
-      username = mkOption {
-        # For `nsupdate` username contains the path to the nsupdate executable
-        default = lib.optionalString (config.services.ddclient.protocol == "nsupdate") "${pkgs.bind.dnsutils}/bin/nsupdate";
-        defaultText = "";
-        type = str;
-        description = lib.mdDoc ''
-          User name.
-        '';
-      };
-
-      passwordFile = mkOption {
-        default = null;
-        type = nullOr str;
-        description = lib.mdDoc ''
-          A file containing the password or a TSIG key in named format when using the nsupdate protocol.
-        '';
-      };
-
-      interval = mkOption {
-        default = "10min";
-        type = str;
-        description = lib.mdDoc ''
-          The interval at which to run the check and update.
-          See {command}`man 7 systemd.time` for the format.
-        '';
-      };
-
-      configFile = mkOption {
-        default = null;
-        type = nullOr path;
-        description = lib.mdDoc ''
-          Path to configuration file.
-          When set this overrides the generated configuration from module options.
-        '';
-        example = "/root/nixos/secrets/ddclient.conf";
-      };
-
-      protocol = mkOption {
-        default = "dyndns2";
-        type = str;
-        description = lib.mdDoc ''
-          Protocol to use with dynamic DNS provider (see https://sourceforge.net/p/ddclient/wiki/protocols).
-        '';
-      };
-
-      server = mkOption {
-        default = "";
-        type = str;
-        description = lib.mdDoc ''
-          Server address.
-        '';
-      };
-
-      ssl = mkOption {
-        default = true;
-        type = bool;
-        description = lib.mdDoc ''
-          Whether to use SSL/TLS to connect to dynamic DNS provider.
-        '';
-      };
-
-      ipv6 = mkOption {
-        default = false;
-        type = bool;
-        description = lib.mdDoc ''
-          Whether to use IPv6.
-        '';
-      };
-
-
-      quiet = mkOption {
-        default = false;
-        type = bool;
-        description = lib.mdDoc ''
-          Print no messages for unnecessary updates.
-        '';
-      };
-
-      script = mkOption {
-        default = "";
-        type = str;
-        description = lib.mdDoc ''
-          script as required by some providers.
-        '';
-      };
-
-      use = mkOption {
-        default = "web, web=checkip.dyndns.com/, web-skip='Current IP Address: '";
-        type = str;
-        description = lib.mdDoc ''
-          Method to determine the IP address to send to the dynamic DNS provider.
-        '';
-      };
-
-      verbose = mkOption {
-        default = false;
-        type = bool;
-        description = lib.mdDoc ''
-          Print verbose information.
-        '';
-      };
-
-      zone = mkOption {
-        default = "";
-        type = str;
-        description = lib.mdDoc ''
-          zone as required by some providers.
-        '';
-      };
-
-      extraConfig = mkOption {
-        default = "";
-        type = lines;
-        description = lib.mdDoc ''
-          Extra configuration. Contents will be added verbatim to the configuration file.
-
-          ::: {.note}
-          `daemon` should not be added here because it does not work great with the systemd-timer approach the service uses.
-          :::
-        '';
-      };
-    };
-  };
-
-
-  ###### implementation
-
-  config = mkIf config.services.ddclient.enable {
-    systemd.services.ddclient = {
-      description = "Dynamic DNS Client";
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
-      restartTriggers = optional (cfg.configFile != null) cfg.configFile;
-
-      serviceConfig = {
-        DynamicUser = true;
-        RuntimeDirectoryMode = "0700";
-        inherit RuntimeDirectory;
-        inherit StateDirectory;
-        Type = "oneshot";
-        ExecStartPre = "!${pkgs.writeShellScript "ddclient-prestart" preStart}";
-        ExecStart = "${lib.getBin cfg.package}/bin/ddclient -file /run/${RuntimeDirectory}/ddclient.conf";
-      };
-    };
-
-    systemd.timers.ddclient = {
-      description = "Run ddclient";
-      wantedBy = [ "timers.target" ];
-      timerConfig = {
-        OnBootSec = cfg.interval;
-        OnUnitInactiveSec = cfg.interval;
-      };
-    };
-  };
-}
diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix
index ac5d45a65e3..8b6d3fc55f3 100644
--- a/nixos/modules/services/networking/dhcpcd.nix
+++ b/nixos/modules/services/networking/dhcpcd.nix
@@ -33,6 +33,13 @@ let
     (if !config.networking.useDHCP && enableDHCP then
       map (i: i.name) (filter (i: i.useDHCP == true) interfaces) else null);
 
+  staticIPv6Addresses = map (i: i.name) (filter (i: i.ipv6.addresses != [ ]) interfaces);
+
+  noIPv6rs = concatStringsSep "\n" (map (name: ''
+    interface ${name}
+    noipv6rs
+  '') staticIPv6Addresses);
+
   # Config file adapted from the one that ships with dhcpcd.
   dhcpcdConf = pkgs.writeText "dhcpcd.conf"
     ''
@@ -74,6 +81,11 @@ let
         noipv6
       ''}
 
+      ${optionalString (config.networking.enableIPv6 && cfg.IPv6rs == null && staticIPv6Addresses != [ ]) noIPv6rs}
+      ${optionalString (config.networking.enableIPv6 && cfg.IPv6rs == false) ''
+        noipv6rs
+      ''}
+
       ${cfg.extraConfig}
     '';
 
@@ -151,6 +163,16 @@ in
       '';
     };
 
+    networking.dhcpcd.IPv6rs = mkOption {
+      type = types.nullOr types.bool;
+      default = null;
+      description = lib.mdDoc ''
+        Force enable or disable solicitation and receipt of IPv6 Router Advertisements.
+        This is required, for example, when using a static unique local IPv6 address (ULA)
+        and global IPv6 address auto-configuration with SLAAC.
+      '';
+    };
+
     networking.dhcpcd.runHook = mkOption {
       type = types.lines;
       default = "";
diff --git a/nixos/modules/services/networking/dhcpd.nix b/nixos/modules/services/networking/dhcpd.nix
index 0bd5e4ef553..a981a255c3e 100644
--- a/nixos/modules/services/networking/dhcpd.nix
+++ b/nixos/modules/services/networking/dhcpd.nix
@@ -218,6 +218,13 @@ in
 
     systemd.services = dhcpdService "4" cfg4 // dhcpdService "6" cfg6;
 
+    warnings = [
+      ''
+        The dhcpd4 and dhcpd6 modules will be removed from NixOS 23.11, because ISC DHCP reached its end of life.
+        See https://www.isc.org/blogs/isc-dhcp-eol/ for details.
+        Please switch to a different implementation like kea, systemd-networkd or dnsmasq.
+      ''
+    ];
   };
 
 }
diff --git a/nixos/modules/services/networking/envoy.nix b/nixos/modules/services/networking/envoy.nix
index 20cfebb7991..c68ceab9619 100644
--- a/nixos/modules/services/networking/envoy.nix
+++ b/nixos/modules/services/networking/envoy.nix
@@ -6,18 +6,29 @@ let
   cfg = config.services.envoy;
   format = pkgs.formats.json { };
   conf = format.generate "envoy.json" cfg.settings;
-  validateConfig = file:
+  validateConfig = required: file:
     pkgs.runCommand "validate-envoy-conf" { } ''
-      ${pkgs.envoy}/bin/envoy --log-level error --mode validate -c "${file}"
+      ${cfg.package}/bin/envoy --log-level error --mode validate -c "${file}" ${lib.optionalString (!required) "|| true"}
       cp "${file}" "$out"
     '';
-
 in
 
 {
   options.services.envoy = {
     enable = mkEnableOption (lib.mdDoc "Envoy reverse proxy");
 
+    package = mkPackageOptionMD pkgs "envoy" { };
+
+    requireValidConfig = mkOption {
+      type = types.bool;
+      default = true;
+      description = lib.mdDoc ''
+        Whether a failure during config validation at build time is fatal.
+        When the config can't be checked during build time, for example when it includes
+        other files, disable this option.
+      '';
+    };
+
     settings = mkOption {
       type = format.type;
       default = { };
@@ -46,38 +57,44 @@ in
   };
 
   config = mkIf cfg.enable {
-    environment.systemPackages = [ pkgs.envoy ];
+    environment.systemPackages = [ cfg.package ];
     systemd.services.envoy = {
       description = "Envoy reverse proxy";
       after = [ "network-online.target" ];
       requires = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
-        ExecStart = "${pkgs.envoy}/bin/envoy -c ${validateConfig conf}";
-        DynamicUser = true;
+        ExecStart = "${cfg.package}/bin/envoy -c ${validateConfig cfg.requireValidConfig conf}";
+        CacheDirectory = [ "envoy" ];
+        LogsDirectory = [ "envoy" ];
         Restart = "no";
-        CacheDirectory = "envoy";
-        LogsDirectory = "envoy";
-        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
-        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
-        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_XDP";
-        SystemCallArchitectures = "native";
+        # Hardening
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+        DeviceAllow = [ "" ];
+        DevicePolicy = "closed";
+        DynamicUser = true;
         LockPersonality = true;
-        RestrictNamespaces = true;
-        RestrictRealtime = true;
-        PrivateUsers = false;  # breaks CAP_NET_BIND_SERVICE
+        MemoryDenyWriteExecute = false; # at least wasmr needs WX permission
         PrivateDevices = true;
+        PrivateUsers = false; # breaks CAP_NET_BIND_SERVICE
+        ProcSubset = "pid";
         ProtectClock = true;
         ProtectControlGroups = true;
         ProtectHome = true;
+        ProtectHostname = true;
         ProtectKernelLogs = true;
         ProtectKernelModules = true;
         ProtectKernelTunables = true;
         ProtectProc = "ptraceable";
-        ProtectHostname = true;
         ProtectSystem = "strict";
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" "AF_XDP" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
         UMask = "0066";
-        SystemCallFilter = "~@clock @module @mount @reboot @swap @obsolete @cpu-emulation";
       };
     };
   };
diff --git a/nixos/modules/services/networking/fakeroute.nix b/nixos/modules/services/networking/fakeroute.nix
index ed6b1a3c4d2..faf5879a6ed 100644
--- a/nixos/modules/services/networking/fakeroute.nix
+++ b/nixos/modules/services/networking/fakeroute.nix
@@ -1,10 +1,8 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   cfg = config.services.fakeroute;
-  routeConf = pkgs.writeText "route.conf" (concatStringsSep "\n" cfg.route);
+  routeConf = pkgs.writeText "route.conf" (lib.concatStringsSep "\n" cfg.route);
 
 in
 
@@ -16,16 +14,10 @@ in
 
     services.fakeroute = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to enable the fakeroute service.
-        '';
-      };
+      enable = lib.mkEnableOption (lib.mdDoc "the fakeroute service");
 
-      route = mkOption {
-        type = types.listOf types.str;
+      route = lib.mkOption {
+        type = with lib.types; listOf str;
         default = [];
         example = [
           "216.102.187.130"
@@ -46,14 +38,16 @@ in
 
   ###### implementation
 
-  config = mkIf cfg.enable {
+  config = lib.mkIf cfg.enable {
     systemd.services.fakeroute = {
       description = "Fakeroute Daemon";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
         Type = "forking";
-        User = "root";
+        User = "fakeroute";
+        DynamicUser = true;
+        AmbientCapabilities = [ "CAP_NET_RAW" ];
         ExecStart = "${pkgs.fakeroute}/bin/fakeroute -f ${routeConf}";
       };
     };
diff --git a/nixos/modules/services/networking/firefox-syncserver.nix b/nixos/modules/services/networking/firefox-syncserver.nix
index 9733fb16d90..42924d7f699 100644
--- a/nixos/modules/services/networking/firefox-syncserver.nix
+++ b/nixos/modules/services/networking/firefox-syncserver.nix
@@ -304,6 +304,10 @@ in
         forceSSL = cfg.singleNode.enableTLS;
         locations."/" = {
           proxyPass = "http://127.0.0.1:${toString cfg.settings.port}";
+          # We need to pass the Host header that matches the original Host header. Otherwise,
+          # Hawk authentication will fail (because it assumes that the client and server see
+          # the same value of the Host header).
+          recommendedProxySettings = true;
         };
       };
     };
@@ -311,8 +315,6 @@ in
 
   meta = {
     maintainers = with lib.maintainers; [ pennae ];
-    # Don't edit the docbook xml directly, edit the md and generate it:
-    # `pandoc firefox-syncserver.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > firefox-syncserver.xml`
-    doc = ./firefox-syncserver.xml;
+    doc = ./firefox-syncserver.md;
   };
 }
diff --git a/nixos/modules/services/networking/firefox-syncserver.xml b/nixos/modules/services/networking/firefox-syncserver.xml
deleted file mode 100644
index 66c81226695..00000000000
--- a/nixos/modules/services/networking/firefox-syncserver.xml
+++ /dev/null
@@ -1,77 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-firefox-syncserver">
-  <title>Firefox Sync server</title>
-  <para>
-    A storage server for Firefox Sync that you can easily host yourself.
-  </para>
-  <section xml:id="module-services-firefox-syncserver-quickstart">
-    <title>Quickstart</title>
-    <para>
-      The absolute minimal configuration for the sync server looks like
-      this:
-    </para>
-    <programlisting language="nix">
-services.mysql.package = pkgs.mariadb;
-
-services.firefox-syncserver = {
-  enable = true;
-  secrets = builtins.toFile &quot;sync-secrets&quot; ''
-    SYNC_MASTER_SECRET=this-secret-is-actually-leaked-to-/nix/store
-  '';
-  singleNode = {
-    enable = true;
-    hostname = &quot;localhost&quot;;
-    url = &quot;http://localhost:5000&quot;;
-  };
-};
-</programlisting>
-    <para>
-      This will start a sync server that is only accessible locally.
-      Once the services is running you can navigate to
-      <literal>about:config</literal> in your Firefox profile and set
-      <literal>identity.sync.tokenserver.uri</literal> to
-      <literal>http://localhost:5000/1.0/sync/1.5</literal>. Your
-      browser will now use your local sync server for data storage.
-    </para>
-    <warning>
-      <para>
-        This configuration should never be used in production. It is not
-        encrypted and stores its secrets in a world-readable location.
-      </para>
-    </warning>
-  </section>
-  <section xml:id="module-services-firefox-syncserver-configuration">
-    <title>More detailed setup</title>
-    <para>
-      The <literal>firefox-syncserver</literal> service provides a
-      number of options to make setting up small deployment easier.
-      These are grouped under the <literal>singleNode</literal> element
-      of the option tree and allow simple configuration of the most
-      important parameters.
-    </para>
-    <para>
-      Single node setup is split into two kinds of options: those that
-      affect the sync server itself, and those that affect its
-      surroundings. Options that affect the sync server are
-      <literal>capacity</literal>, which configures how many accounts
-      may be active on this instance, and <literal>url</literal>, which
-      holds the URL under which the sync server can be accessed. The
-      <literal>url</literal> can be configured automatically when using
-      nginx.
-    </para>
-    <para>
-      Options that affect the surroundings of the sync server are
-      <literal>enableNginx</literal>, <literal>enableTLS</literal> and
-      <literal>hostnam</literal>. If <literal>enableNginx</literal> is
-      set the sync server module will automatically add an nginx virtual
-      host to the system using <literal>hostname</literal> as the domain
-      and set <literal>url</literal> accordingly. If
-      <literal>enableTLS</literal> is set the module will also enable
-      ACME certificates on the new virtual host and force all
-      connections to be made via TLS.
-    </para>
-    <para>
-      For actual deployment it is also recommended to store the
-      <literal>secrets</literal> file in a secure location.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/networking/firewall-nftables.nix b/nixos/modules/services/networking/firewall-nftables.nix
index 0ed3c228075..452dd97d89d 100644
--- a/nixos/modules/services/networking/firewall-nftables.nix
+++ b/nixos/modules/services/networking/firewall-nftables.nix
@@ -94,7 +94,13 @@ in
           ${optionalString (ifaceSet != "") ''iifname { ${ifaceSet} } accept comment "trusted interfaces"''}
 
           # Some ICMPv6 types like NDP is untracked
-          ct state vmap { invalid : drop, established : accept, related : accept, * : jump input-allow } comment "*: new and untracked"
+          ct state vmap {
+            invalid : drop,
+            established : accept,
+            related : accept,
+            new : jump input-allow,
+            untracked: jump input-allow,
+          }
 
           ${optionalString cfg.logRefusedConnections ''
             tcp flags syn / fin,syn,rst,ack log level info prefix "refused connection: "
@@ -143,7 +149,13 @@ in
           chain forward {
             type filter hook forward priority filter; policy drop;
 
-            ct state vmap { invalid : drop, established : accept, related : accept, * : jump forward-allow } comment "*: new and untracked"
+            ct state vmap {
+              invalid : drop,
+              established : accept,
+              related : accept,
+              new : jump forward-allow,
+              untracked : jump forward-allow,
+            }
 
           }
 
diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix
index 4e332d489e4..ac02a93836b 100644
--- a/nixos/modules/services/networking/firewall.nix
+++ b/nixos/modules/services/networking/firewall.nix
@@ -269,6 +269,10 @@ in
         assertion = cfg.filterForward -> config.networking.nftables.enable;
         message = "filterForward only works with the nftables based firewall";
       }
+      {
+        assertion = cfg.autoLoadConntrackHelpers -> lib.versionOlder config.boot.kernelPackages.kernel.version "6";
+        message = "conntrack helper autoloading has been removed from kernel 6.0 and newer";
+      }
     ];
 
     networking.firewall.trustedInterfaces = [ "lo" ];
diff --git a/nixos/modules/services/networking/gnunet.nix b/nixos/modules/services/networking/gnunet.nix
index 9d1c9746f72..fdb353fd344 100644
--- a/nixos/modules/services/networking/gnunet.nix
+++ b/nixos/modules/services/networking/gnunet.nix
@@ -124,8 +124,8 @@ in
         type = types.lines;
         default = "";
         description = lib.mdDoc ''
-          Additional options that will be copied verbatim in `gnunet.conf'.
-          See `gnunet.conf(5)' for details.
+          Additional options that will be copied verbatim in `gnunet.conf`.
+          See {manpage}`gnunet.conf(5)` for details.
         '';
       };
     };
@@ -155,7 +155,7 @@ in
       description = "GNUnet";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      restartTriggers = [ configFile ];
+      restartTriggers = [ config.environment.etc."gnunet.conf".source ];
       path = [ cfg.package pkgs.miniupnpc ];
       serviceConfig.ExecStart = "${cfg.package}/lib/gnunet/libexec/gnunet-service-arm -c /etc/gnunet.conf";
       serviceConfig.User = "gnunet";
diff --git a/nixos/modules/services/networking/go-neb.nix b/nixos/modules/services/networking/go-neb.nix
index 8c04542c47c..b65bb5f548e 100644
--- a/nixos/modules/services/networking/go-neb.nix
+++ b/nixos/modules/services/networking/go-neb.nix
@@ -60,13 +60,12 @@ in {
 
       serviceConfig = {
         ExecStartPre = lib.optional (cfg.secretFile != null)
-          (pkgs.writeShellScript "pre-start" ''
+          ("+" + pkgs.writeShellScript "pre-start" ''
             umask 077
             export $(xargs < ${cfg.secretFile})
             ${pkgs.envsubst}/bin/envsubst -i "${configFile}" > ${finalConfigFile}
             chown go-neb ${finalConfigFile}
           '');
-        PermissionsStartOnly = true;
         RuntimeDirectory = "go-neb";
         ExecStart = "${pkgs.go-neb}/bin/go-neb";
         User = "go-neb";
diff --git a/nixos/modules/services/networking/harmonia.nix b/nixos/modules/services/networking/harmonia.nix
new file mode 100644
index 00000000000..144fa6c708e
--- /dev/null
+++ b/nixos/modules/services/networking/harmonia.nix
@@ -0,0 +1,88 @@
+{ config, pkgs, lib, ... }:
+let
+  cfg = config.services.harmonia;
+  format = pkgs.formats.toml { };
+in
+{
+  options = {
+    services.harmonia = {
+      enable = lib.mkEnableOption (lib.mdDoc "Harmonia: Nix binary cache written in Rust");
+
+      signKeyPath = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        description = lib.mdDoc "Path to the signing key that will be used for signing the cache";
+      };
+
+      package = lib.mkPackageOptionMD pkgs "harmonia" { };
+
+      settings = lib.mkOption {
+        inherit (format) type;
+        default = { };
+        description = lib.mdDoc ''
+          Settings to merge with the default configuration.
+          For the list of the default configuration, see <https://github.com/nix-community/harmonia/tree/master#configuration>.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.harmonia = {
+      description = "harmonia binary cache service";
+
+      requires = [ "nix-daemon.socket" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        CONFIG_FILE = format.generate "harmonia.toml" cfg.settings;
+        SIGN_KEY_PATH = lib.mkIf (cfg.signKeyPath != null) "%d/sign-key";
+        # Note: it's important to set this for nix-store, because it wants to use
+        # $HOME in order to use a temporary cache dir. bizarre failures will occur
+        # otherwise
+        HOME = "/run/harmonia";
+      };
+
+      serviceConfig = {
+        ExecStart = lib.getExe cfg.package;
+        User = "harmonia";
+        Group = "harmonia";
+        DynamicUser = true;
+        PrivateUsers = true;
+        DeviceAllow = [ "" ];
+        UMask = "0066";
+        RuntimeDirectory = "harmonia";
+        LoadCredential = lib.mkIf (cfg.signKeyPath != null) [ "sign-key:${cfg.signKeyPath}" ];
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+          "~@resources"
+        ];
+        CapabilityBoundingSet = "";
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        ProtectKernelLogs = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        RestrictRealtime = true;
+        MemoryDenyWriteExecute = true;
+        ProcSubset = "pid";
+        ProtectProc = "invisible";
+        RestrictNamespaces = true;
+        SystemCallArchitectures = "native";
+        PrivateNetwork = false;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        LockPersonality = true;
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
+        LimitNOFILE = 65536;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/headscale.nix b/nixos/modules/services/networking/headscale.nix
index cc46819eed5..78253dd9d11 100644
--- a/nixos/modules/services/networking/headscale.nix
+++ b/nixos/modules/services/networking/headscale.nix
@@ -291,25 +291,59 @@ in {
                 '';
               };
 
-              client_secret_file = mkOption {
+              client_secret_path = mkOption {
                 type = types.nullOr types.path;
                 default = null;
                 description = lib.mdDoc ''
-                  Path to OpenID Connect client secret file.
+                  Path to OpenID Connect client secret file. Expands environment variables in format ''${VAR}.
                 '';
               };
 
-              domain_map = mkOption {
+              scope = mkOption {
+                type = types.listOf types.str;
+                default = ["openid" "profile" "email"];
+                description = lib.mdDoc ''
+                  Scopes used in the OIDC flow.
+                '';
+              };
+
+              extra_params = mkOption {
                 type = types.attrsOf types.str;
-                default = {};
+                default = { };
                 description = lib.mdDoc ''
-                  Domain map is used to map incomming users (by their email) to
-                  a namespace. The key can be a string, or regex.
+                  Custom query parameters to send with the Authorize Endpoint request.
                 '';
                 example = {
-                  ".*" = "default-namespace";
+                  domain_hint = "example.com";
                 };
               };
+
+              allowed_domains = mkOption {
+                type = types.listOf types.str;
+                default = [ ];
+                description = lib.mdDoc ''
+                  Allowed principal domains. if an authenticated user's domain
+                  is not in this list authentication request will be rejected.
+                '';
+                example = [ "example.com" ];
+              };
+
+              allowed_users = mkOption {
+                type = types.listOf types.str;
+                default = [ ];
+                description = lib.mdDoc ''
+                  Users allowed to authenticate even if not in allowedDomains.
+                '';
+                example = [ "alice@example.com" ];
+              };
+
+              strip_email_domain = mkOption {
+                type = types.bool;
+                default = true;
+                description = lib.mdDoc ''
+                  Whether the domain part of the email address should be removed when generating namespaces.
+                '';
+              };
             };
 
             tls_letsencrypt_hostname = mkOption {
@@ -359,7 +393,7 @@ in {
               type = types.nullOr types.path;
               default = null;
               description = lib.mdDoc ''
-                Path to a file containg ACL policies.
+                Path to a file containing ACL policies.
               '';
             };
           };
@@ -391,14 +425,17 @@ in {
     (mkRenamedOptionModule ["services" "headscale" "dns" "baseDomain"] ["services" "headscale" "settings" "dns_config" "base_domain"])
     (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "issuer"] ["services" "headscale" "settings" "oidc" "issuer"])
     (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "clientId"] ["services" "headscale" "settings" "oidc" "client_id"])
-    (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "clientSecretFile"] ["services" "headscale" "settings" "oidc" "client_secret_file"])
-    (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "domainMap"] ["services" "headscale" "settings" "oidc" "domain_map"])
+    (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "clientSecretFile"] ["services" "headscale" "settings" "oidc" "client_secret_path"])
     (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "hostname"] ["services" "headscale" "settings" "tls_letsencrypt_hostname"])
     (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "challengeType"] ["services" "headscale" "settings" "tls_letsencrypt_challenge_type"])
     (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "httpListen"] ["services" "headscale" "settings" "tls_letsencrypt_listen"])
     (mkRenamedOptionModule ["services" "headscale" "tls" "certFile"] ["services" "headscale" "settings" "tls_cert_path"])
     (mkRenamedOptionModule ["services" "headscale" "tls" "keyFile"] ["services" "headscale" "settings" "tls_key_path"])
     (mkRenamedOptionModule ["services" "headscale" "aclPolicyFile"] ["services" "headscale" "settings" "acl_policy_path"])
+
+    (mkRemovedOptionModule ["services" "headscale" "openIdConnect" "domainMap"] ''
+      Headscale no longer uses domain_map. If you're using an old version of headscale you can still set this option via services.headscale.settings.oidc.domain_map.
+    '')
   ];
 
   config = mkIf cfg.enable {
@@ -441,9 +478,6 @@ in {
           export HEADSCALE_DB_PASS="$(head -n1 ${escapeShellArg cfg.settings.db_password_file})"
         ''}
 
-        ${optionalString (cfg.settings.oidc.client_secret_file != null) ''
-          export HEADSCALE_OIDC_CLIENT_SECRET="$(head -n1 ${escapeShellArg cfg.settings.oidc.client_secret_file})"
-        ''}
         exec ${cfg.package}/bin/headscale serve
       '';
 
diff --git a/nixos/modules/services/networking/hostapd.nix b/nixos/modules/services/networking/hostapd.nix
index 63bb44256dd..45e32dc3b31 100644
--- a/nixos/modules/services/networking/hostapd.nix
+++ b/nixos/modules/services/networking/hostapd.nix
@@ -1,219 +1,1285 @@
 { config, lib, pkgs, utils, ... }:
+# All hope abandon ye who enter here. hostapd's configuration
+# format is ... special, and you won't be able to infer any
+# of their assumptions from just reading the "documentation"
+# (i.e. the example config). Assume footguns at all points -
+# to make informed decisions you will probably need to look
+# at hostapd's code. You have been warned, proceed with care.
+let
+  inherit
+    (lib)
+    attrNames
+    attrValues
+    concatLists
+    concatMap
+    concatMapStrings
+    concatStringsSep
+    count
+    escapeShellArg
+    filter
+    flip
+    generators
+    getAttr
+    hasPrefix
+    imap0
+    isInt
+    isString
+    length
+    literalExpression
+    maintainers
+    mapAttrsToList
+    mdDoc
+    mkDefault
+    mkEnableOption
+    mkIf
+    mkOption
+    mkPackageOption
+    mkRemovedOptionModule
+    optional
+    optionalAttrs
+    optionalString
+    optionals
+    singleton
+    stringLength
+    toLower
+    types
+    unique
+    ;
 
-# TODO:
-#
-# asserts
-#   ensure that the nl80211 module is loaded/compiled in the kernel
-#   wpa_supplicant and hostapd on the same wireless interface doesn't make any sense
+  cfg = config.services.hostapd;
 
-with lib;
+  extraSettingsFormat = {
+    type = let
+      singleAtom = types.oneOf [ types.bool types.int types.str ];
+      atom = types.either singleAtom (types.listOf singleAtom) // {
+        description = "atom (bool, int or string) or a list of them for duplicate keys";
+      };
+    in types.attrsOf atom;
 
-let
+    generate = name: value: pkgs.writeText name (generators.toKeyValue {
+      listsAsDuplicateKeys = true;
+      mkKeyValue = generators.mkKeyValueDefault {
+        mkValueString = v:
+          if      isInt    v then toString v
+          else if isString v then v
+          else if true  == v then "1"
+          else if false == v then "0"
+          else throw "unsupported type ${builtins.typeOf v}: ${(generators.toPretty {}) v}";
+      } "=";
+    } value);
+  };
 
-  cfg = config.services.hostapd;
+  # Generates the header for a single BSS (i.e. WiFi network)
+  writeBssHeader = radio: bss: bssIdx: pkgs.writeText "hostapd-radio-${radio}-bss-${bss}.conf" ''
+    ''\n''\n# BSS ${toString bssIdx}: ${bss}
+    ################################
 
-  escapedInterface = utils.escapeSystemdPath cfg.interface;
+    ${if bssIdx == 0 then "interface" else "bss"}=${bss}
+  '';
 
-  configFile = pkgs.writeText "hostapd.conf" ''
-    interface=${cfg.interface}
-    driver=${cfg.driver}
-    ssid=${cfg.ssid}
-    hw_mode=${cfg.hwMode}
-    channel=${toString cfg.channel}
-    ${optionalString (cfg.countryCode != null) "country_code=${cfg.countryCode}"}
-    ${optionalString (cfg.countryCode != null) "ieee80211d=1"}
+  makeRadioRuntimeFiles = radio: radioCfg:
+    pkgs.writeShellScript "make-hostapd-${radio}-files" (''
+      set -euo pipefail
 
-    # logging (debug level)
-    logger_syslog=-1
-    logger_syslog_level=${toString cfg.logLevel}
-    logger_stdout=-1
-    logger_stdout_level=${toString cfg.logLevel}
+      hostapd_config_file=/run/hostapd/${escapeShellArg radio}.hostapd.conf
+      rm -f "$hostapd_config_file"
+      cat > "$hostapd_config_file" <<EOF
+      # Radio base configuration: ${radio}
+      ################################
 
-    ctrl_interface=/run/hostapd
-    ctrl_interface_group=${cfg.group}
+      EOF
 
-    ${optionalString cfg.wpa ''
-      wpa=2
-      wpa_passphrase=${cfg.wpaPassphrase}
-    ''}
-    ${optionalString cfg.noScan "noscan=1"}
+      cat ${escapeShellArg (extraSettingsFormat.generate "hostapd-radio-${radio}-extra.conf" radioCfg.settings)} >> "$hostapd_config_file"
+      ${concatMapStrings (script: "${script} \"$hostapd_config_file\"\n") (attrValues radioCfg.dynamicConfigScripts)}
+    ''
+    + concatMapStrings (x: "${x}\n") (imap0 (i: f: f i)
+      (mapAttrsToList (bss: bssCfg: bssIdx: ''
+        ''\n# BSS configuration: ${bss}
 
-    ${cfg.extraConfig}
-  '' ;
+        mac_allow_file=/run/hostapd/${escapeShellArg bss}.mac.allow
+        rm -f "$mac_allow_file"
+        touch "$mac_allow_file"
 
-in
+        mac_deny_file=/run/hostapd/${escapeShellArg bss}.mac.deny
+        rm -f "$mac_deny_file"
+        touch "$mac_deny_file"
 
-{
-  ###### interface
+        cat ${writeBssHeader radio bss bssIdx} >> "$hostapd_config_file"
+        cat ${escapeShellArg (extraSettingsFormat.generate "hostapd-radio-${radio}-bss-${bss}-extra.conf" bssCfg.settings)} >> "$hostapd_config_file"
+        ${concatMapStrings (script: "${script} \"$hostapd_config_file\" \"$mac_allow_file\" \"$mac_deny_file\"\n") (attrValues bssCfg.dynamicConfigScripts)}
+      '') radioCfg.networks)));
 
-  options = {
+  runtimeConfigFiles = mapAttrsToList (radio: _: "/run/hostapd/${radio}.hostapd.conf") cfg.radios;
+in {
+  meta.maintainers = with maintainers; [ oddlama ];
 
+  options = {
     services.hostapd = {
+      enable = mkEnableOption (mdDoc ''
+        Whether to enable hostapd. hostapd is a user space daemon for access point and
+        authentication servers. It implements IEEE 802.11 access point management,
+        IEEE 802.1X/WPA/WPA2/EAP Authenticators, RADIUS client, EAP server, and RADIUS
+        authentication server.
+      '');
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Enable putting a wireless interface into infrastructure mode,
-          allowing other wireless devices to associate with the wireless
-          interface and do wireless networking. A simple access point will
-          {option}`enable hostapd.wpa`,
-          {option}`hostapd.wpaPassphrase`, and
-          {option}`hostapd.ssid`, as well as DHCP on the wireless
-          interface to provide IP addresses to the associated stations, and
-          NAT (from the wireless interface to an upstream interface).
-        '';
-      };
+      package = mkPackageOption pkgs "hostapd" {};
 
-      interface = mkOption {
-        default = "";
-        example = "wlp2s0";
-        type = types.str;
-        description = lib.mdDoc ''
-          The interfaces {command}`hostapd` will use.
-        '';
-      };
+      radios = mkOption {
+        default = {};
+        example = literalExpression ''
+          {
+            # Simple 2.4GHz AP
+            wlp2s0 = {
+              # countryCode = "US";
+              networks.wlp2s0 = {
+                ssid = "AP 1";
+                authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible.
+              };
+            };
 
-      noScan = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Do not scan for overlapping BSSs in HT40+/- mode.
-          Caution: turning this on will violate regulatory requirements!
-        '';
-      };
+            # WiFi 5 (5GHz) with two advertised networks
+            wlp3s0 = {
+              band = "5g";
+              channel = 0; # Enable automatic channel selection (ACS). Use only if your hardware supports it.
+              # countryCode = "US";
+              networks.wlp3s0 = {
+                ssid = "My AP";
+                authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible.
+              };
+              networks.wlp3s0-1 = {
+                ssid = "Open AP with WiFi5";
+                authentication.mode = "none";
+              };
+            };
 
-      driver = mkOption {
-        default = "nl80211";
-        example = "hostapd";
-        type = types.str;
-        description = lib.mdDoc ''
-          Which driver {command}`hostapd` will use.
-          Most applications will probably use the default.
+            # Legacy WPA2 example
+            wlp4s0 = {
+              # countryCode = "US";
+              networks.wlp4s0 = {
+                ssid = "AP 2";
+                authentication = {
+                  mode = "wpa2-sha256";
+                  wpaPassword = "a flakey password"; # Use wpaPasswordFile if possible.
+                };
+              };
+            };
+          }
         '';
-      };
+        description = mdDoc ''
+          This option allows you to define APs for one or multiple physical radios.
+          At least one radio must be specified.
 
-      ssid = mkOption {
-        default = "nixos";
-        example = "mySpecialSSID";
-        type = types.str;
-        description = lib.mdDoc "SSID to be used in IEEE 802.11 management frames.";
-      };
+          For each radio, hostapd requires a separate logical interface (like wlp3s0, wlp3s1, ...).
+          A default interface is usually be created automatically by your system, but to use
+          multiple radios of a single device, it may be required to create additional logical interfaces
+          for example by using {option}`networking.wlanInterfaces`.
 
-      hwMode = mkOption {
-        default = "g";
-        type = types.enum [ "a" "b" "g" ];
-        description = lib.mdDoc ''
-          Operation mode.
-          (a = IEEE 802.11a, b = IEEE 802.11b, g = IEEE 802.11g).
+          Each physical radio can only support a single hardware-mode that is configured via
+          ({option}`services.hostapd.radios.<radio>.band`). To create a dual-band
+          or tri-band AP, you will have to use a device that has multiple physical radios
+          and supports configuring multiple APs (Refer to valid interface combinations in
+          {command}`iw list`).
         '';
-      };
+        type = types.attrsOf (types.submodule (radioSubmod: {
+          options = {
+            driver = mkOption {
+              default = "nl80211";
+              example = "none";
+              type = types.str;
+              description = mdDoc ''
+                The driver {command}`hostapd` will use.
+                {var}`nl80211` is used with all Linux mac80211 drivers.
+                {var}`none` is used if building a standalone RADIUS server that does
+                not control any wireless/wired driver.
+                Most applications will probably use the default.
+              '';
+            };
 
-      channel = mkOption {
-        default = 7;
-        example = 11;
-        type = types.int;
-        description = lib.mdDoc ''
-          Channel number (IEEE 802.11)
-          Please note that some drivers do not use this value from
-          {command}`hostapd` and the channel will need to be configured
-          separately with {command}`iwconfig`.
-        '';
-      };
+            noScan = mkOption {
+              type = types.bool;
+              default = false;
+              description = mdDoc ''
+                Disables scan for overlapping BSSs in HT40+/- mode.
+                Caution: turning this on will likely violate regulatory requirements!
+              '';
+            };
 
-      group = mkOption {
-        default = "wheel";
-        example = "network";
-        type = types.str;
-        description = lib.mdDoc ''
-          Members of this group can control {command}`hostapd`.
-        '';
-      };
+            countryCode = mkOption {
+              default = null;
+              example = "US";
+              type = types.nullOr types.str;
+              description = mdDoc ''
+                Country code (ISO/IEC 3166-1). Used to set regulatory domain.
+                Set as needed to indicate country in which device is operating.
+                This can limit available channels and transmit power.
+                These two octets are used as the first two octets of the Country String
+                (dot11CountryString).
 
-      wpa = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Enable WPA (IEEE 802.11i/D3.0) to authenticate with the access point.
-        '';
-      };
+                Setting this will force you to also enable IEEE 802.11d and IEEE 802.11h.
 
-      wpaPassphrase = mkOption {
-        default = "my_sekret";
-        example = "any_64_char_string";
-        type = types.str;
-        description = lib.mdDoc ''
-          WPA-PSK (pre-shared-key) passphrase. Clients will need this
-          passphrase to associate with this access point.
-          Warning: This passphrase will get put into a world-readable file in
-          the Nix store!
-        '';
-      };
+                IEEE 802.11d: This advertises the countryCode and the set of allowed channels
+                and transmit power levels based on the regulatory limits.
 
-      logLevel = mkOption {
-        default = 2;
-        type = types.int;
-        description = lib.mdDoc ''
-          Levels (minimum value for logged events):
-          0 = verbose debugging
-          1 = debugging
-          2 = informational messages
-          3 = notification
-          4 = warning
-        '';
-      };
+                IEEE802.11h: This enables radar detection and DFS (Dynamic Frequency Selection)
+                support if available. DFS support is required on outdoor 5 GHz channels in most
+                countries of the world.
+              '';
+            };
 
-      countryCode = mkOption {
-        default = null;
-        example = "US";
-        type = with types; nullOr str;
-        description = lib.mdDoc ''
-          Country code (ISO/IEC 3166-1). Used to set regulatory domain.
-          Set as needed to indicate country in which device is operating.
-          This can limit available channels and transmit power.
-          These two octets are used as the first two octets of the Country String
-          (dot11CountryString).
-          If set this enables IEEE 802.11d. This advertises the countryCode and
-          the set of allowed channels and transmit power levels based on the
-          regulatory limits.
-        '';
-      };
+            band = mkOption {
+              default = "2g";
+              type = types.enum ["2g" "5g" "6g" "60g"];
+              description = mdDoc ''
+                Specifies the frequency band to use, possible values are 2g for 2.4 GHz,
+                5g for 5 GHz, 6g for 6 GHz and 60g for 60 GHz.
+              '';
+            };
+
+            channel = mkOption {
+              default = 7;
+              example = 11;
+              type = types.int;
+              description = mdDoc ''
+                The channel to operate on. Use 0 to enable ACS (Automatic Channel Selection).
+                Beware that not every device supports ACS in which case {command}`hostapd`
+                will fail to start.
+              '';
+            };
+
+            settings = mkOption {
+              default = {};
+              example = { acs_exclude_dfs = true; };
+              type = types.submodule {
+                freeformType = extraSettingsFormat.type;
+              };
+              description = mdDoc ''
+                Extra configuration options to put at the end of global initialization, before defining BSSs.
+                To find out which options are global and which are per-bss you have to read hostapd's source code,
+                which is non-trivial and not documented otherwise.
+
+                Lists will be converted to multiple definitions of the same key, and booleans to 0/1.
+                Otherwise, the inputs are not modified or checked for correctness.
+              '';
+            };
+
+            dynamicConfigScripts = mkOption {
+              default = {};
+              type = types.attrsOf types.path;
+              example = literalExpression ''
+                {
+                  exampleDynamicConfig = pkgs.writeShellScript "dynamic-config" '''
+                    HOSTAPD_CONFIG=$1
+
+                    cat >> "$HOSTAPD_CONFIG" << EOF
+                    # Add some dynamically generated statements here,
+                    # for example based on the physical adapter in use
+                    EOF
+                  ''';
+                }
+              '';
+              description = mdDoc ''
+                All of these scripts will be executed in lexicographical order before hostapd
+                is started, right after the global segment was generated and may dynamically
+                append global options the generated configuration file.
+
+                The first argument will point to the configuration file that you may append to.
+              '';
+            };
+
+            #### IEEE 802.11n (WiFi 4) related configuration
+
+            wifi4 = {
+              enable = mkOption {
+                default = true;
+                type = types.bool;
+                description = mdDoc ''
+                  Enables support for IEEE 802.11n (WiFi 4, HT).
+                  This is enabled by default, since the vase majority of devices
+                  are expected to support this.
+                '';
+              };
+
+              capabilities = mkOption {
+                type = types.listOf types.str;
+                default = ["HT40" "HT40-" "SHORT-GI-20" "SHORT-GI-40"];
+                example = ["LDPC" "HT40+" "HT40-" "GF" "SHORT-GI-20" "SHORT-GI-40" "TX-STBC" "RX-STBC1"];
+                description = mdDoc ''
+                  HT (High Throughput) capabilities given as a list of flags.
+                  Please refer to the hostapd documentation for allowed values and
+                  only set values supported by your physical adapter.
+
+                  The default contains common values supported by most adapters.
+                '';
+              };
+
+              require = mkOption {
+                default = false;
+                type = types.bool;
+                description = mdDoc "Require stations (clients) to support WiFi 4 (HT) and disassociate them if they don't.";
+              };
+            };
+
+            #### IEEE 802.11ac (WiFi 5) related configuration
+
+            wifi5 = {
+              enable = mkOption {
+                default = true;
+                type = types.bool;
+                description = mdDoc "Enables support for IEEE 802.11ac (WiFi 5, VHT)";
+              };
+
+              capabilities = mkOption {
+                type = types.listOf types.str;
+                default = [];
+                example = ["SHORT-GI-80" "TX-STBC-2BY1" "RX-STBC-1" "RX-ANTENNA-PATTERN" "TX-ANTENNA-PATTERN"];
+                description = mdDoc ''
+                  VHT (Very High Throughput) capabilities given as a list of flags.
+                  Please refer to the hostapd documentation for allowed values and
+                  only set values supported by your physical adapter.
+                '';
+              };
+
+              require = mkOption {
+                default = false;
+                type = types.bool;
+                description = mdDoc "Require stations (clients) to support WiFi 5 (VHT) and disassociate them if they don't.";
+              };
+
+              operatingChannelWidth = mkOption {
+                default = "20or40";
+                type = types.enum ["20or40" "80" "160" "80+80"];
+                apply = x:
+                  getAttr x {
+                    "20or40" = 0;
+                    "80" = 1;
+                    "160" = 2;
+                    "80+80" = 3;
+                  };
+                description = mdDoc ''
+                  Determines the operating channel width for VHT.
+
+                  - {var}`"20or40"`: 20 or 40 MHz operating channel width
+                  - {var}`"80"`: 80 MHz channel width
+                  - {var}`"160"`: 160 MHz channel width
+                  - {var}`"80+80"`: 80+80 MHz channel width
+                '';
+              };
+            };
+
+            #### IEEE 802.11ax (WiFi 6) related configuration
+
+            wifi6 = {
+              enable = mkOption {
+                default = false;
+                type = types.bool;
+                description = mdDoc "Enables support for IEEE 802.11ax (WiFi 6, HE)";
+              };
+
+              require = mkOption {
+                default = false;
+                type = types.bool;
+                description = mdDoc "Require stations (clients) to support WiFi 6 (HE) and disassociate them if they don't.";
+              };
+
+              singleUserBeamformer = mkOption {
+                default = false;
+                type = types.bool;
+                description = mdDoc "HE single user beamformer support";
+              };
+
+              singleUserBeamformee = mkOption {
+                default = false;
+                type = types.bool;
+                description = mdDoc "HE single user beamformee support";
+              };
+
+              multiUserBeamformer = mkOption {
+                default = false;
+                type = types.bool;
+                description = mdDoc "HE multi user beamformee support";
+              };
+
+              operatingChannelWidth = mkOption {
+                default = "20or40";
+                type = types.enum ["20or40" "80" "160" "80+80"];
+                apply = x:
+                  getAttr x {
+                    "20or40" = 0;
+                    "80" = 1;
+                    "160" = 2;
+                    "80+80" = 3;
+                  };
+                description = mdDoc ''
+                  Determines the operating channel width for HE.
+
+                  - {var}`"20or40"`: 20 or 40 MHz operating channel width
+                  - {var}`"80"`: 80 MHz channel width
+                  - {var}`"160"`: 160 MHz channel width
+                  - {var}`"80+80"`: 80+80 MHz channel width
+                '';
+              };
+            };
+
+            #### IEEE 802.11be (WiFi 7) related configuration
+
+            wifi7 = {
+              enable = mkOption {
+                default = false;
+                type = types.bool;
+                description = mdDoc ''
+                  Enables support for IEEE 802.11be (WiFi 7, EHT). This is currently experimental
+                  and requires you to manually enable CONFIG_IEEE80211BE when building hostapd.
+                '';
+              };
+
+              singleUserBeamformer = mkOption {
+                default = false;
+                type = types.bool;
+                description = mdDoc "EHT single user beamformer support";
+              };
+
+              singleUserBeamformee = mkOption {
+                default = false;
+                type = types.bool;
+                description = mdDoc "EHT single user beamformee support";
+              };
+
+              multiUserBeamformer = mkOption {
+                default = false;
+                type = types.bool;
+                description = mdDoc "EHT multi user beamformee support";
+              };
+
+              operatingChannelWidth = mkOption {
+                default = "20or40";
+                type = types.enum ["20or40" "80" "160" "80+80"];
+                apply = x:
+                  getAttr x {
+                    "20or40" = 0;
+                    "80" = 1;
+                    "160" = 2;
+                    "80+80" = 3;
+                  };
+                description = mdDoc ''
+                  Determines the operating channel width for EHT.
+
+                  - {var}`"20or40"`: 20 or 40 MHz operating channel width
+                  - {var}`"80"`: 80 MHz channel width
+                  - {var}`"160"`: 160 MHz channel width
+                  - {var}`"80+80"`: 80+80 MHz channel width
+                '';
+              };
+            };
+
+            #### BSS definitions
+
+            networks = mkOption {
+              default = {};
+              example = literalExpression ''
+                {
+                  wlp2s0 = {
+                    ssid = "Primary advertised network";
+                    authentication.saePasswords = [{ password = "a flakey password"; }]; # Use saePasswordsFile if possible.
+                  };
+                  wlp2s0-1 = {
+                    ssid = "Secondary advertised network (Open)";
+                    authentication.mode = "none";
+                  };
+                }
+              '';
+              description = mdDoc ''
+                This defines a BSS, colloquially known as a WiFi network.
+                You have to specify at least one.
+              '';
+              type = types.attrsOf (types.submodule (bssSubmod: {
+                options = {
+                  logLevel = mkOption {
+                    default = 2;
+                    type = types.int;
+                    description = mdDoc ''
+                      Levels (minimum value for logged events):
+                      0 = verbose debugging
+                      1 = debugging
+                      2 = informational messages
+                      3 = notification
+                      4 = warning
+                    '';
+                  };
+
+                  group = mkOption {
+                    default = "wheel";
+                    example = "network";
+                    type = types.str;
+                    description = mdDoc ''
+                      Members of this group can access the control socket for this interface.
+                    '';
+                  };
+
+                  utf8Ssid = mkOption {
+                    default = true;
+                    type = types.bool;
+                    description = mdDoc "Whether the SSID is to be interpreted using UTF-8 encoding.";
+                  };
+
+                  ssid = mkOption {
+                    example = "❄️ cool ❄️";
+                    type = types.str;
+                    description = mdDoc "SSID to be used in IEEE 802.11 management frames.";
+                  };
 
-      extraConfig = mkOption {
-        default = "";
-        example = ''
-          auth_algo=0
-          ieee80211n=1
-          ht_capab=[HT40-][SHORT-GI-40][DSSS_CCK-40]
-          '';
-        type = types.lines;
-        description = lib.mdDoc "Extra configuration options to put in hostapd.conf.";
+                  bssid = mkOption {
+                    type = types.nullOr types.str;
+                    default = null;
+                    example = "11:22:33:44:55:66";
+                    description = mdDoc ''
+                      Specifies the BSSID for this BSS. Usually determined automatically,
+                      but for now you have to manually specify them when using multiple BSS.
+                      Try assigning related addresses from the locally administered MAC address ranges,
+                      by reusing the hardware address but replacing the second nibble with 2, 6, A or E.
+                      (e.g. if real address is `XX:XX:XX:XX:XX`, try `X2:XX:XX:XX:XX:XX`, `X6:XX:XX:XX:XX:XX`, ...
+                      for the second, third, ... BSS)
+                    '';
+                  };
+
+                  macAcl = mkOption {
+                    default = "deny";
+                    type = types.enum ["deny" "allow" "radius"];
+                    apply = x:
+                      getAttr x {
+                        "deny" = 0;
+                        "allow" = 1;
+                        "radius" = 2;
+                      };
+                    description = mdDoc ''
+                      Station MAC address -based authentication. The following modes are available:
+
+                      - {var}`"deny"`: Allow unless listed in {option}`macDeny` (default)
+                      - {var}`"allow"`: Deny unless listed in {option}`macAllow`
+                      - {var}`"radius"`: Use external radius server, but check both {option}`macAllow` and {option}`macDeny` first
+
+                      Please note that this kind of access control requires a driver that uses
+                      hostapd to take care of management frame processing and as such, this can be
+                      used with driver=hostap or driver=nl80211, but not with driver=atheros.
+                    '';
+                  };
+
+                  macAllow = mkOption {
+                    type = types.listOf types.str;
+                    default = [];
+                    example = ["11:22:33:44:55:66"];
+                    description = mdDoc ''
+                      Specifies the MAC addresses to allow if {option}`macAcl` is set to {var}`"allow"` or {var}`"radius"`.
+                      These values will be world-readable in the Nix store. Values will automatically be merged with
+                      {option}`macAllowFile` if necessary.
+                    '';
+                  };
+
+                  macAllowFile = mkOption {
+                    type = types.nullOr types.path;
+                    default = null;
+                    description = mdDoc ''
+                      Specifies a file containing the MAC addresses to allow if {option}`macAcl` is set to {var}`"allow"` or {var}`"radius"`.
+                      The file should contain exactly one MAC address per line. Comments and empty lines are ignored,
+                      only lines starting with a valid MAC address will be considered (e.g. `11:22:33:44:55:66`) and
+                      any content after the MAC address is ignored.
+                    '';
+                  };
+
+                  macDeny = mkOption {
+                    type = types.listOf types.str;
+                    default = [];
+                    example = ["11:22:33:44:55:66"];
+                    description = mdDoc ''
+                      Specifies the MAC addresses to deny if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`.
+                      These values will be world-readable in the Nix store. Values will automatically be merged with
+                      {option}`macDenyFile` if necessary.
+                    '';
+                  };
+
+                  macDenyFile = mkOption {
+                    type = types.nullOr types.path;
+                    default = null;
+                    description = mdDoc ''
+                      Specifies a file containing the MAC addresses to deny if {option}`macAcl` is set to {var}`"deny"` or {var}`"radius"`.
+                      The file should contain exactly one MAC address per line. Comments and empty lines are ignored,
+                      only lines starting with a valid MAC address will be considered (e.g. `11:22:33:44:55:66`) and
+                      any content after the MAC address is ignored.
+                    '';
+                  };
+
+                  ignoreBroadcastSsid = mkOption {
+                    default = "disabled";
+                    type = types.enum ["disabled" "empty" "clear"];
+                    apply = x:
+                      getAttr x {
+                        "disabled" = 0;
+                        "empty" = 1;
+                        "clear" = 2;
+                      };
+                    description = mdDoc ''
+                      Send empty SSID in beacons and ignore probe request frames that do not
+                      specify full SSID, i.e., require stations to know SSID. Note that this does
+                      not increase security, since your clients will then broadcast the SSID instead,
+                      which can increase congestion.
+
+                      - {var}`"disabled"`: Advertise ssid normally.
+                      - {var}`"empty"`: send empty (length=0) SSID in beacon and ignore probe request for broadcast SSID
+                      - {var}`"clear"`: clear SSID (ASCII 0), but keep the original length (this may be required with some
+                        legacy clients that do not support empty SSID) and ignore probe requests for broadcast SSID. Only
+                        use this if empty does not work with your clients.
+                    '';
+                  };
+
+                  apIsolate = mkOption {
+                    default = false;
+                    type = types.bool;
+                    description = mdDoc ''
+                      Isolate traffic between stations (clients) and prevent them from
+                      communicating with each other.
+                    '';
+                  };
+
+                  settings = mkOption {
+                    default = {};
+                    example = { multi_ap = true; };
+                    type = types.submodule {
+                      freeformType = extraSettingsFormat.type;
+                    };
+                    description = mdDoc ''
+                      Extra configuration options to put at the end of this BSS's defintion in the
+                      hostapd.conf for the associated interface. To find out which options are global
+                      and which are per-bss you have to read hostapd's source code, which is non-trivial
+                      and not documented otherwise.
+
+                      Lists will be converted to multiple definitions of the same key, and booleans to 0/1.
+                      Otherwise, the inputs are not modified or checked for correctness.
+                    '';
+                  };
+
+                  dynamicConfigScripts = mkOption {
+                    default = {};
+                    type = types.attrsOf types.path;
+                    example = literalExpression ''
+                      {
+                        exampleDynamicConfig = pkgs.writeShellScript "dynamic-config" '''
+                          HOSTAPD_CONFIG=$1
+                          # These always exist, but may or may not be used depending on the actual configuration
+                          MAC_ALLOW_FILE=$2
+                          MAC_DENY_FILE=$3
+
+                          cat >> "$HOSTAPD_CONFIG" << EOF
+                          # Add some dynamically generated statements here
+                          EOF
+                        ''';
+                      }
+                    '';
+                    description = mdDoc ''
+                      All of these scripts will be executed in lexicographical order before hostapd
+                      is started, right after the bss segment was generated and may dynamically
+                      append bss options to the generated configuration file.
+
+                      The first argument will point to the configuration file that you may append to.
+                      The second and third argument will point to this BSS's MAC allow and MAC deny file respectively.
+                    '';
+                  };
+
+                  #### IEEE 802.11i (WPA) configuration
+
+                  authentication = {
+                    mode = mkOption {
+                      default = "wpa3-sae";
+                      type = types.enum ["none" "wpa2-sha256" "wpa3-sae-transition" "wpa3-sae"];
+                      description = mdDoc ''
+                        Selects the authentication mode for this AP.
+
+                        - {var}`"none"`: Don't configure any authentication. This will disable wpa alltogether
+                          and create an open AP. Use {option}`settings` together with this option if you
+                          want to configure the authentication manually. Any password options will still be
+                          effective, if set.
+                        - {var}`"wpa2-sha256"`: WPA2-Personal using SHA256 (IEEE 802.11i/RSN). Passwords are set
+                          using {option}`wpaPassword` or preferably by {option}`wpaPasswordFile` or {option}`wpaPskFile`.
+                        - {var}`"wpa3-sae-transition"`: Use WPA3-Personal (SAE) if possible, otherwise fallback
+                          to WPA2-SHA256. Only use if necessary and switch to the newer WPA3-SAE when possible.
+                          You will have to specify both {option}`wpaPassword` and {option}`saePasswords` (or one of their alternatives).
+                        - {var}`"wpa3-sae"`: Use WPA3-Personal (SAE). This is currently the recommended way to
+                          setup a secured WiFi AP (as of March 2023) and therefore the default. Passwords are set
+                          using either {option}`saePasswords` or preferably {option}`saePasswordsFile`.
+                      '';
+                    };
+
+                    pairwiseCiphers = mkOption {
+                      default = ["CCMP"];
+                      example = ["CCMP-256" "GCMP-256"];
+                      type = types.listOf types.str;
+                      description = mdDoc ''
+                        Set of accepted cipher suites (encryption algorithms) for pairwise keys (unicast packets).
+                        By default this allows just CCMP, which is the only commonly supported secure option.
+                        Use {option}`enableRecommendedPairwiseCiphers` to also enable newer recommended ciphers.
+
+                        Please refer to the hostapd documentation for allowed values. Generally, only
+                        CCMP or GCMP modes should be considered safe options. Most devices support CCMP while
+                        GCMP is often only available with devices supporting WiFi 5 (IEEE 802.11ac) or higher.
+                      '';
+                    };
+
+                    enableRecommendedPairwiseCiphers = mkOption {
+                      default = false;
+                      example = true;
+                      type = types.bool;
+                      description = mdDoc ''
+                        Additionally enable the recommended set of pairwise ciphers.
+                        This enables newer secure ciphers, additionally to those defined in {option}`pairwiseCiphers`.
+                        You will have to test whether your hardware supports these by trial-and-error, because
+                        even if `iw list` indicates hardware support, your driver might not expose it.
+
+                        Beware {command}`hostapd` will most likely not return a useful error message in case
+                        this is enabled despite the driver or hardware not supporting the newer ciphers.
+                        Look out for messages like `Failed to set beacon parameters`.
+                      '';
+                    };
+
+                    wpaPassword = mkOption {
+                      default = null;
+                      example = "a flakey password";
+                      type = types.nullOr types.str;
+                      description = mdDoc ''
+                        Sets the password for WPA-PSK that will be converted to the pre-shared key.
+                        The password length must be in the range [8, 63] characters. While some devices
+                        may allow arbitrary characters (such as UTF-8) to be used, but the standard specifies
+                        that each character in the passphrase must be an ASCII character in the range [0x20, 0x7e]
+                        (IEEE Std. 802.11i-2004, Annex H.4.1). Use emojis at your own risk.
+
+                        Not used when {option}`mode` is {var}`"wpa3-sae"`.
+
+                        Warning: This password will get put into a world-readable file in the Nix store!
+                        Using {option}`wpaPasswordFile` or {option}`wpaPskFile` instead is recommended.
+                      '';
+                    };
+
+                    wpaPasswordFile = mkOption {
+                      default = null;
+                      type = types.nullOr types.path;
+                      description = mdDoc ''
+                        Sets the password for WPA-PSK. Follows the same rules as {option}`wpaPassword`,
+                        but reads the password from the given file to prevent the password from being
+                        put into the Nix store.
+
+                        Not used when {option}`mode` is {var}`"wpa3-sae"`.
+                      '';
+                    };
+
+                    wpaPskFile = mkOption {
+                      default = null;
+                      type = types.nullOr types.path;
+                      description = mdDoc ''
+                        Sets the password(s) for WPA-PSK. Similar to {option}`wpaPasswordFile`,
+                        but additionally allows specifying multiple passwords, and some other options.
+
+                        Each line, except for empty lines and lines starting with #, must contain a
+                        MAC address and either a 64-hex-digit PSK or a password separated with a space.
+                        The password must follow the same rules as outlined in {option}`wpaPassword`.
+                        The special MAC address `00:00:00:00:00:00` can be used to configure PSKs
+                        that any client can use.
+
+                        An optional key identifier can be added by prefixing the line with `keyid=<keyid_string>`
+                        An optional VLAN ID can be specified by prefixing the line with `vlanid=<VLAN ID>`.
+                        An optional WPS tag can be added by prefixing the line with `wps=<0/1>` (default: 0).
+                        Any matching entry with that tag will be used when generating a PSK for a WPS Enrollee
+                        instead of generating a new random per-Enrollee PSK.
+
+                        Not used when {option}`mode` is {var}`"wpa3-sae"`.
+                      '';
+                    };
+
+                    saePasswords = mkOption {
+                      default = [];
+                      example = literalExpression ''
+                        [
+                          # Any client may use these passwords
+                          { password = "Wi-Figure it out"; }
+                          { password = "second password for everyone"; mac = "ff:ff:ff:ff:ff:ff"; }
+
+                          # Only the client with MAC-address 11:22:33:44:55:66 can use this password
+                          { password = "sekret pazzword"; mac = "11:22:33:44:55:66"; }
+                        ]
+                      '';
+                      description = mdDoc ''
+                        Sets allowed passwords for WPA3-SAE.
+
+                        The last matching (based on peer MAC address and identifier) entry is used to
+                        select which password to use. An empty string has the special meaning of
+                        removing all previously added entries.
+
+                        Warning: These entries will get put into a world-readable file in
+                        the Nix store! Using {option}`saePasswordFile` instead is recommended.
+
+                        Not used when {option}`mode` is {var}`"wpa2-sha256"`.
+                      '';
+                      type = types.listOf (types.submodule {
+                        options = {
+                          password = mkOption {
+                            example = "a flakey password";
+                            type = types.str;
+                            description = mdDoc ''
+                              The password for this entry. SAE technically imposes no restrictions on
+                              password length or character set. But due to limitations of {command}`hostapd`'s
+                              config file format, a true newline character cannot be parsed.
+
+                              Warning: This password will get put into a world-readable file in
+                              the Nix store! Using {option}`wpaPasswordFile` or {option}`wpaPskFile` is recommended.
+                            '';
+                          };
+
+                          mac = mkOption {
+                            default = null;
+                            example = "11:22:33:44:55:66";
+                            type = types.nullOr types.str;
+                            description = mdDoc ''
+                              If this attribute is not included, or if is set to the wildcard address (`ff:ff:ff:ff:ff:ff`),
+                              the entry is available for any station (client) to use. If a specific peer MAC address is included,
+                              only a station with that MAC address is allowed to use the entry.
+                            '';
+                          };
+
+                          vlanid = mkOption {
+                            default = null;
+                            example = 1;
+                            type = types.nullOr types.int;
+                            description = mdDoc "If this attribute is given, all clients using this entry will get tagged with the given VLAN ID.";
+                          };
+
+                          pk = mkOption {
+                            default = null;
+                            example = "";
+                            type = types.nullOr types.str;
+                            description = mdDoc ''
+                              If this attribute is given, SAE-PK will be enabled for this connection.
+                              This prevents evil-twin attacks, but a public key is required additionally to connect.
+                              (Essentially adds pubkey authentication such that the client can verify identity of the AP)
+                            '';
+                          };
+
+                          id = mkOption {
+                            default = null;
+                            example = "";
+                            type = types.nullOr types.str;
+                            description = mdDoc ''
+                              If this attribute is given with non-zero length, it will set the password identifier
+                              for this entry. It can then only be used with that identifier.
+                            '';
+                          };
+                        };
+                      });
+                    };
+
+                    saePasswordsFile = mkOption {
+                      default = null;
+                      type = types.nullOr types.path;
+                      description = mdDoc ''
+                        Sets the password for WPA3-SAE. Follows the same rules as {option}`saePasswords`,
+                        but reads the entries from the given file to prevent them from being
+                        put into the Nix store.
+
+                        One entry per line, empty lines and lines beginning with # will be ignored.
+                        Each line must match the following format, although the order of optional
+                        parameters doesn't matter:
+                        `<password>[|mac=<peer mac>][|vlanid=<VLAN ID>][|pk=<m:ECPrivateKey-base64>][|id=<identifier>]`
+
+                        Not used when {option}`mode` is {var}`"wpa2-sha256"`.
+                      '';
+                    };
+
+                    saeAddToMacAllow = mkOption {
+                      type = types.bool;
+                      default = false;
+                      description = mdDoc ''
+                        If set, all sae password entries that have a non-wildcard MAC associated to
+                        them will additionally be used to populate the MAC allow list. This is
+                        additional to any entries set via {option}`macAllow` or {option}`macAllowFile`.
+                      '';
+                    };
+                  };
+
+                  managementFrameProtection = mkOption {
+                    default = "required";
+                    type = types.enum ["disabled" "optional" "required"];
+                    apply = x:
+                      getAttr x {
+                        "disabled" = 0;
+                        "optional" = 1;
+                        "required" = 2;
+                      };
+                    description = mdDoc ''
+                      Management frame protection (MFP) authenticates management frames
+                      to prevent deauthentication (or related) attacks.
+
+                      - {var}`"disabled"`: No management frame protection
+                      - {var}`"optional"`: Use MFP if a connection allows it
+                      - {var}`"required"`: Force MFP for all clients
+                    '';
+                  };
+                };
+
+                config = let
+                  bss = bssSubmod.name;
+                  bssCfg = bssSubmod.config;
+
+                  pairwiseCiphers =
+                    concatStringsSep " " (unique (bssCfg.authentication.pairwiseCiphers
+                      ++ optionals bssCfg.authentication.enableRecommendedPairwiseCiphers ["CCMP" "CCMP-256" "GCMP" "GCMP-256"]));
+                in {
+                  settings = {
+                    ssid = bssCfg.ssid;
+                    utf8_ssid = bssCfg.ssid;
+
+                    logger_syslog = mkDefault (-1);
+                    logger_syslog_level = bssCfg.logLevel;
+                    logger_stdout = mkDefault (-1);
+                    logger_stdout_level = bssCfg.logLevel;
+                    ctrl_interface = mkDefault "/run/hostapd";
+                    ctrl_interface_group = bssCfg.group;
+
+                    macaddr_acl = bssCfg.macAcl;
+
+                    ignore_broadcast_ssid = bssCfg.ignoreBroadcastSsid;
+
+                    # IEEE 802.11i (authentication) related configuration
+                    # Encrypt management frames to protect against deauthentication and similar attacks
+                    ieee80211w = bssCfg.managementFrameProtection;
+
+                    # Only allow WPA by default and disable insecure WEP
+                    auth_algs = mkDefault 1;
+                    # Always enable QoS, which is required for 802.11n and above
+                    wmm_enabled = mkDefault true;
+                    ap_isolate = bssCfg.apIsolate;
+
+                    sae_password = flip map bssCfg.authentication.saePasswords (
+                      entry:
+                        entry.password
+                        + optionalString (entry.mac != null) "|mac=${entry.mac}"
+                        + optionalString (entry.vlanid != null) "|vlanid=${toString entry.vlanid}"
+                        + optionalString (entry.pk != null) "|pk=${entry.pk}"
+                        + optionalString (entry.id != null) "|id=${entry.id}"
+                    );
+                  } // optionalAttrs (bssCfg.bssid != null) {
+                    bssid = bssCfg.bssid;
+                  } // optionalAttrs (bssCfg.macAllow != [] || bssCfg.macAllowFile != null || bssCfg.authentication.saeAddToMacAllow) {
+                    accept_mac_file = "/run/hostapd/${bss}.mac.allow";
+                  } // optionalAttrs (bssCfg.macDeny != [] || bssCfg.macDenyFile != null) {
+                    deny_mac_file = "/run/hostapd/${bss}.mac.deny";
+                  } // optionalAttrs (bssCfg.authentication.mode == "none") {
+                    wpa = mkDefault 0;
+                  } // optionalAttrs (bssCfg.authentication.mode == "wpa3-sae") {
+                    wpa = 2;
+                    wpa_key_mgmt = "SAE";
+                    # Derive PWE using both hunting-and-pecking loop and hash-to-element
+                    sae_pwe = 2;
+                    # Prevent downgrade attacks by indicating to clients that they should
+                    # disable any transition modes from now on.
+                    transition_disable = "0x01";
+                  } // optionalAttrs (bssCfg.authentication.mode == "wpa3-sae-transition") {
+                    wpa = 2;
+                    wpa_key_mgmt = "WPA-PSK-SHA256 SAE";
+                  } // optionalAttrs (bssCfg.authentication.mode == "wpa2-sha256") {
+                    wpa = 2;
+                    wpa_key_mgmt = "WPA-PSK-SHA256";
+                  } // optionalAttrs (bssCfg.authentication.mode != "none") {
+                    wpa_pairwise = pairwiseCiphers;
+                    rsn_pairwise = pairwiseCiphers;
+                  } // optionalAttrs (bssCfg.authentication.wpaPassword != null) {
+                    wpa_passphrase = bssCfg.authentication.wpaPassword;
+                  } // optionalAttrs (bssCfg.authentication.wpaPskFile != null) {
+                    wpa_psk_file = bssCfg.authentication.wpaPskFile;
+                  };
+
+                  dynamicConfigScripts = let
+                    # All MAC addresses from SAE entries that aren't the wildcard address
+                    saeMacs = filter (mac: mac != null && (toLower mac) != "ff:ff:ff:ff:ff:ff") (map (x: x.mac) bssCfg.authentication.saePasswords);
+                  in {
+                    "20-addMacAllow" = mkIf (bssCfg.macAllow != []) (pkgs.writeShellScript "add-mac-allow" ''
+                      MAC_ALLOW_FILE=$2
+                      cat >> "$MAC_ALLOW_FILE" <<EOF
+                      ${concatStringsSep "\n" bssCfg.macAllow}
+                      EOF
+                    '');
+                    "20-addMacAllowFile" = mkIf (bssCfg.macAllowFile != null) (pkgs.writeShellScript "add-mac-allow-file" ''
+                      MAC_ALLOW_FILE=$2
+                      grep -Eo '^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' ${escapeShellArg bssCfg.macAllowFile} >> "$MAC_ALLOW_FILE"
+                    '');
+                    "20-addMacAllowFromSae" = mkIf (bssCfg.authentication.saeAddToMacAllow && saeMacs != []) (pkgs.writeShellScript "add-mac-allow-from-sae" ''
+                      MAC_ALLOW_FILE=$2
+                      cat >> "$MAC_ALLOW_FILE" <<EOF
+                      ${concatStringsSep "\n" saeMacs}
+                      EOF
+                    '');
+                    # Populate mac allow list from saePasswordsFile
+                    # (filter for lines with mac=;  exclude commented lines; filter for real mac-addresses; strip mac=)
+                    "20-addMacAllowFromSaeFile" = mkIf (bssCfg.authentication.saeAddToMacAllow && bssCfg.authentication.saePasswordsFile != null) (pkgs.writeShellScript "add-mac-allow-from-sae-file" ''
+                      MAC_ALLOW_FILE=$2
+                      grep mac= ${escapeShellArg bssCfg.authentication.saePasswordsFile} \
+                        | grep -v '\s*#' \
+                        | grep -Eo 'mac=([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' \
+                        | sed 's|^mac=||' >> "$MAC_ALLOW_FILE"
+                    '');
+                    "20-addMacDeny" = mkIf (bssCfg.macDeny != []) (pkgs.writeShellScript "add-mac-deny" ''
+                      MAC_DENY_FILE=$3
+                      cat >> "$MAC_DENY_FILE" <<EOF
+                      ${concatStringsSep "\n" bssCfg.macDeny}
+                      EOF
+                    '');
+                    "20-addMacDenyFile" = mkIf (bssCfg.macDenyFile != null) (pkgs.writeShellScript "add-mac-deny-file" ''
+                      MAC_DENY_FILE=$3
+                      grep -Eo '^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})' ${escapeShellArg bssCfg.macDenyFile} >> "$MAC_DENY_FILE"
+                    '');
+                    # Add wpa_passphrase from file
+                    "20-wpaPasswordFile" = mkIf (bssCfg.authentication.wpaPasswordFile != null) (pkgs.writeShellScript "wpa-password-file" ''
+                      HOSTAPD_CONFIG_FILE=$1
+                      cat >> "$HOSTAPD_CONFIG_FILE" <<EOF
+                      wpa_passphrase=$(cat ${escapeShellArg bssCfg.authentication.wpaPasswordFile})
+                      EOF
+                    '');
+                    # Add sae passwords from file
+                    "20-saePasswordsFile" = mkIf (bssCfg.authentication.saePasswordsFile != null) (pkgs.writeShellScript "sae-passwords-file" ''
+                      HOSTAPD_CONFIG_FILE=$1
+                      grep -v '\s*#' ${escapeShellArg bssCfg.authentication.saePasswordsFile} \
+                        | sed 's/^/sae_password=/' >> "$HOSTAPD_CONFIG_FILE"
+                    '');
+                  };
+                };
+              }));
+            };
+          };
+
+          config.settings = let
+            radio = radioSubmod.name;
+            radioCfg = radioSubmod.config;
+          in {
+            driver = radioCfg.driver;
+            hw_mode = {
+              "2g" = "g";
+              "5g" = "a";
+              "6g" = "a";
+              "60g" = "ad";
+            }.${radioCfg.band};
+            channel = radioCfg.channel;
+            noscan = radioCfg.noScan;
+          } // optionalAttrs (radioCfg.countryCode != null) {
+            country_code = radioCfg.countryCode;
+            # IEEE 802.11d: Limit to frequencies allowed in country
+            ieee80211d = true;
+            # IEEE 802.11h: Enable radar detection and DFS (Dynamic Frequency Selection)
+            ieee80211h = true;
+          } // optionalAttrs radioCfg.wifi4.enable {
+            # IEEE 802.11n (WiFi 4) related configuration
+            ieee80211n = true;
+            require_ht = radioCfg.wifi4.require;
+            ht_capab = concatMapStrings (x: "[${x}]") radioCfg.wifi4.capabilities;
+          } // optionalAttrs radioCfg.wifi5.enable {
+            # IEEE 802.11ac (WiFi 5) related configuration
+            ieee80211ac = true;
+            require_vht = radioCfg.wifi5.require;
+            vht_oper_chwidth = radioCfg.wifi5.operatingChannelWidth;
+            vht_capab = concatMapStrings (x: "[${x}]") radioCfg.wifi5.capabilities;
+          } // optionalAttrs radioCfg.wifi6.enable {
+            # IEEE 802.11ax (WiFi 6) related configuration
+            ieee80211ax = true;
+            require_he = mkIf radioCfg.wifi6.require true;
+            he_oper_chwidth = radioCfg.wifi6.operatingChannelWidth;
+            he_su_beamformer = radioCfg.wifi6.singleUserBeamformer;
+            he_su_beamformee = radioCfg.wifi6.singleUserBeamformee;
+            he_mu_beamformer = radioCfg.wifi6.multiUserBeamformer;
+          } // optionalAttrs radioCfg.wifi7.enable {
+            # IEEE 802.11be (WiFi 7) related configuration
+            ieee80211be = true;
+            eht_oper_chwidth = radioCfg.wifi7.operatingChannelWidth;
+            eht_su_beamformer = radioCfg.wifi7.singleUserBeamformer;
+            eht_su_beamformee = radioCfg.wifi7.singleUserBeamformee;
+            eht_mu_beamformer = radioCfg.wifi7.multiUserBeamformer;
+          };
+        }));
       };
     };
   };
 
+  imports = let
+    renamedOptionMessage = message: ''
+      ${message}
+      Refer to the documentation of `services.hostapd.radios` for an example and more information.
+    '';
+  in [
+    (mkRemovedOptionModule ["services" "hostapd" "interface"]
+      (renamedOptionMessage "All other options for this interface are now set via `services.hostapd.radios.«interface».*`."))
+
+    (mkRemovedOptionModule ["services" "hostapd" "driver"]
+      (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».driver`."))
+    (mkRemovedOptionModule ["services" "hostapd" "noScan"]
+      (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».noScan`."))
+    (mkRemovedOptionModule ["services" "hostapd" "countryCode"]
+      (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».countryCode`."))
+    (mkRemovedOptionModule ["services" "hostapd" "hwMode"]
+      (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».band`."))
+    (mkRemovedOptionModule ["services" "hostapd" "channel"]
+      (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».channel`."))
+    (mkRemovedOptionModule ["services" "hostapd" "extraConfig"]
+      (renamedOptionMessage ''
+        It has been replaced by `services.hostapd.radios.«interface».settings` and
+        `services.hostapd.radios.«interface».networks.«network».settings` respectively
+        for per-radio and per-network extra configuration. The module now supports a lot more
+        options inherently, so please re-check whether using settings is still necessary.''))
+
+    (mkRemovedOptionModule ["services" "hostapd" "logLevel"]
+      (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».logLevel`."))
+    (mkRemovedOptionModule ["services" "hostapd" "group"]
+      (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».group`."))
+    (mkRemovedOptionModule ["services" "hostapd" "ssid"]
+      (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».ssid`."))
 
-  ###### implementation
+    (mkRemovedOptionModule ["services" "hostapd" "wpa"]
+      (renamedOptionMessage "It has been replaced by `services.hostapd.radios.«interface».networks.«network».authentication.mode`."))
+    (mkRemovedOptionModule ["services" "hostapd" "wpaPassphrase"]
+      (renamedOptionMessage ''
+        It has been replaced by `services.hostapd.radios.«interface».networks.«network».authentication.wpaPassword`.
+        While upgrading your config, please consider using the newer SAE authentication scheme
+        and one of the new `passwordFile`-like options to avoid putting the password into the world readable nix-store.''))
+  ];
 
   config = mkIf cfg.enable {
+    assertions =
+      [
+        {
+          assertion = cfg.radios != {};
+          message = "At least one radio must be configured with hostapd!";
+        }
+      ]
+      # Radio warnings
+      ++ (concatLists (mapAttrsToList (
+          radio: radioCfg:
+            [
+              {
+                assertion = radioCfg.networks != {};
+                message = "hostapd radio ${radio}: At least one network must be configured!";
+              }
+              # XXX: There could be many more useful assertions about (band == xy) -> ensure other required settings.
+              # see https://github.com/openwrt/openwrt/blob/539cb5389d9514c99ec1f87bd4465f77c7ed9b93/package/kernel/mac80211/files/lib/netifd/wireless/mac80211.sh#L158
+              {
+                assertion = length (filter (bss: bss == radio) (attrNames radioCfg.networks)) == 1;
+                message = ''hostapd radio ${radio}: Exactly one network must be named like the radio, for reasons internal to hostapd.'';
+              }
+              {
+                assertion = (radioCfg.wifi4.enable && builtins.elem "HT40-" radioCfg.wifi4.capabilities) -> radioCfg.channel != 0;
+                message = ''hostapd radio ${radio}: using ACS (channel = 0) together with HT40- (wifi4.capabilities) is unsupported by hostapd'';
+              }
+            ]
+            # BSS warnings
+            ++ (concatLists (mapAttrsToList (bss: bssCfg: let
+                auth = bssCfg.authentication;
+                countWpaPasswordDefinitions = count (x: x != null) [
+                  auth.wpaPassword
+                  auth.wpaPasswordFile
+                  auth.wpaPskFile
+                ];
+              in [
+                {
+                  assertion = hasPrefix radio bss;
+                  message = "hostapd radio ${radio} bss ${bss}: The bss (network) name ${bss} is invalid. It must be prefixed by the radio name for reasons internal to hostapd. A valid name would be e.g. ${radio}, ${radio}-1, ...";
+                }
+                {
+                  assertion = (length (attrNames radioCfg.networks) > 1) -> (bssCfg.bssid != null);
+                  message = ''hostapd radio ${radio} bss ${bss}: bssid must be specified manually (for now) since this radio uses multiple BSS.'';
+                }
+                {
+                  assertion = auth.mode == "wpa3-sae" -> bssCfg.managementFrameProtection == 2;
+                  message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE which requires managementFrameProtection="required"'';
+                }
+                {
+                  assertion = auth.mode == "wpa3-sae-transition" -> bssCfg.managementFrameProtection != 0;
+                  message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode with WPA2-SHA256, which requires managementFrameProtection="optional" or ="required"'';
+                }
+                {
+                  assertion = countWpaPasswordDefinitions <= 1;
+                  message = ''hostapd radio ${radio} bss ${bss}: must use at most one WPA password option (wpaPassword, wpaPasswordFile, wpaPskFile)'';
+                }
+                {
+                  assertion = auth.wpaPassword != null -> (stringLength auth.wpaPassword >= 8 && stringLength auth.wpaPassword <= 63);
+                  message = ''hostapd radio ${radio} bss ${bss}: uses a wpaPassword of invalid length (must be in [8,63]).'';
+                }
+                {
+                  assertion = auth.saePasswords == [] || auth.saePasswordsFile == null;
+                  message = ''hostapd radio ${radio} bss ${bss}: must use only one SAE password option (saePasswords or saePasswordsFile)'';
+                }
+                {
+                  assertion = auth.mode == "wpa3-sae" -> (auth.saePasswords != [] || auth.saePasswordsFile != null);
+                  message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE which requires defining a sae password option'';
+                }
+                {
+                  assertion = auth.mode == "wpa3-sae-transition" -> (auth.saePasswords != [] || auth.saePasswordsFile != null) && countWpaPasswordDefinitions == 1;
+                  message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode requires defining both a wpa password option and a sae password option'';
+                }
+                {
+                  assertion = auth.mode == "wpa2-sha256" -> countWpaPasswordDefinitions == 1;
+                  message = ''hostapd radio ${radio} bss ${bss}: uses WPA2-SHA256 which requires defining a wpa password option'';
+                }
+              ])
+              radioCfg.networks))
+        )
+        cfg.radios));
 
-    environment.systemPackages =  [ pkgs.hostapd ];
+    environment.systemPackages = [cfg.package];
 
-    services.udev.packages = optionals (cfg.countryCode != null) [ pkgs.crda ];
+    services.udev.packages = with pkgs; [crda];
 
-    systemd.services.hostapd =
-      { description = "hostapd wireless AP";
+    systemd.services.hostapd = {
+      description = "IEEE 802.11 Host Access-Point Daemon";
 
-        path = [ pkgs.hostapd ];
-        after = [ "sys-subsystem-net-devices-${escapedInterface}.device" ];
-        bindsTo = [ "sys-subsystem-net-devices-${escapedInterface}.device" ];
-        requiredBy = [ "network-link-${cfg.interface}.service" ];
-        wantedBy = [ "multi-user.target" ];
+      path = [cfg.package];
+      after = map (radio: "sys-subsystem-net-devices-${utils.escapeSystemdPath radio}.device") (attrNames cfg.radios);
+      bindsTo = map (radio: "sys-subsystem-net-devices-${utils.escapeSystemdPath radio}.device") (attrNames cfg.radios);
+      wantedBy = ["multi-user.target"];
 
-        serviceConfig =
-          { ExecStart = "${pkgs.hostapd}/bin/hostapd ${configFile}";
-            Restart = "always";
-          };
+      # Create merged configuration and acl files for each radio (and their bss's) prior to starting
+      preStart = concatStringsSep "\n" (mapAttrsToList makeRadioRuntimeFiles cfg.radios);
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/hostapd ${concatStringsSep " " runtimeConfigFiles}";
+        Restart = "always";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        RuntimeDirectory = "hostapd";
+
+        # Hardening
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        DevicePolicy = "closed";
+        DeviceAllow = "/dev/rfkill rw";
+        NoNewPrivileges = true;
+        PrivateUsers = false; # hostapd requires true root access.
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        ProtectSystem = "strict";
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_NETLINK"
+          "AF_UNIX"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+          "@chown"
+        ];
+        UMask = "0077";
       };
+    };
   };
 }
diff --git a/nixos/modules/services/networking/i2pd.nix b/nixos/modules/services/networking/i2pd.nix
index 3f6cb97296b..c940324ad09 100644
--- a/nixos/modules/services/networking/i2pd.nix
+++ b/nixos/modules/services/networking/i2pd.nix
@@ -169,15 +169,15 @@ let
         (boolOpt "enabled" proto.enable)
         (strOpt "address" proto.address)
         (intOpt "port" proto.port)
-        ] ++ (if proto ? keys then optionalNullString "keys" proto.keys else [])
-        ++ (if proto ? auth then optionalNullBool "auth" proto.auth else [])
-        ++ (if proto ? user then optionalNullString "user" proto.user else [])
-        ++ (if proto ? pass then optionalNullString "pass" proto.pass else [])
-        ++ (if proto ? strictHeaders then optionalNullBool "strictheaders" proto.strictHeaders else [])
-        ++ (if proto ? hostname then optionalNullString "hostname" proto.hostname else [])
-        ++ (if proto ? outproxy then optionalNullString "outproxy" proto.outproxy else [])
-        ++ (if proto ? outproxyPort then optionalNullInt "outproxyport" proto.outproxyPort else [])
-        ++ (if proto ? outproxyEnable then optionalNullBool "outproxy.enabled" proto.outproxyEnable else []);
+        ] ++ (optionals (proto ? keys) (optionalNullString "keys" proto.keys))
+        ++ (optionals (proto ? auth) (optionalNullBool "auth" proto.auth))
+        ++ (optionals (proto ? user) (optionalNullString "user" proto.user))
+        ++ (optionals (proto ? pass) (optionalNullString "pass" proto.pass))
+        ++ (optionals (proto ? strictHeaders) (optionalNullBool "strictheaders" proto.strictHeaders))
+        ++ (optionals (proto ? hostname) (optionalNullString "hostname" proto.hostname))
+        ++ (optionals (proto ? outproxy) (optionalNullString "outproxy" proto.outproxy))
+        ++ (optionals (proto ? outproxyPort) (optionalNullInt "outproxyport" proto.outproxyPort))
+        ++ (optionals (proto ? outproxyEnable) (optionalNullBool "outproxy.enabled" proto.outproxyEnable));
         in (concatStringsSep "\n" protoOpts)
       ));
   in
@@ -192,21 +192,14 @@ let
         "type = client"
         (intOpt "port" tun.port)
         (strOpt "destination" tun.destination)
-        ] ++ (if tun ? destinationPort then optionalNullInt "destinationport" tun.destinationPort else [])
-        ++ (if tun ? keys then
-            optionalNullString "keys" tun.keys else [])
-        ++ (if tun ? address then
-            optionalNullString "address" tun.address else [])
-        ++ (if tun ? inbound.length then
-            optionalNullInt "inbound.length" tun.inbound.length else [])
-        ++ (if tun ? inbound.quantity then
-            optionalNullInt "inbound.quantity" tun.inbound.quantity else [])
-        ++ (if tun ? outbound.length then
-            optionalNullInt "outbound.length" tun.outbound.length else [])
-        ++ (if tun ? outbound.quantity then
-            optionalNullInt "outbound.quantity" tun.outbound.quantity else [])
-        ++ (if tun ? crypto.tagsToSend then
-            optionalNullInt "crypto.tagstosend" tun.crypto.tagsToSend else []);
+        ] ++ (optionals (tun ? destinationPort) (optionalNullInt "destinationport" tun.destinationPort))
+        ++ (optionals (tun ? keys) (optionalNullString "keys" tun.keys))
+        ++ (optionals (tun ? address) (optionalNullString "address" tun.address))
+        ++ (optionals (tun ? inbound.length) (optionalNullInt "inbound.length" tun.inbound.length))
+        ++ (optionals (tun ? inbound.quantity) (optionalNullInt "inbound.quantity" tun.inbound.quantity))
+        ++ (optionals (tun ? outbound.length) (optionalNullInt "outbound.length" tun.outbound.length))
+        ++ (optionals (tun ? outbound.quantity) (optionalNullInt "outbound.quantity" tun.outbound.quantity))
+        ++ (optionals (tun ? crypto.tagsToSend) (optionalNullInt "crypto.tagstosend" tun.crypto.tagsToSend));
         in concatStringsSep "\n" outTunOpts))
     (flip map
       (collect (tun: tun ? port && tun ? address) cfg.inTunnels)
@@ -215,14 +208,10 @@ let
         "type = server"
         (intOpt "port" tun.port)
         (strOpt "host" tun.address)
-      ] ++ (if tun ? destination then
-            optionalNullString "destination" tun.destination else [])
-        ++ (if tun ? keys then
-            optionalNullString "keys" tun.keys else [])
-        ++ (if tun ? inPort then
-            optionalNullInt "inport" tun.inPort else [])
-        ++ (if tun ? accessList then
-            optionalEmptyList "accesslist" tun.accessList else []);
+      ] ++ (optionals (tun ? destination) (optionalNullString "destination" tun.destination))
+        ++ (optionals (tun ? keys) (optionalNullString "keys" tun.keys))
+        ++ (optionals (tun ? inPort) (optionalNullInt "inport" tun.inPort))
+        ++ (optionals (tun ? accessList) (optionalEmptyList "accesslist" tun.accessList));
         in concatStringsSep "\n" inTunOpts))];
     in pkgs.writeText "i2pd-tunnels.conf" opts;
 
diff --git a/nixos/modules/services/networking/imaginary.nix b/nixos/modules/services/networking/imaginary.nix
new file mode 100644
index 00000000000..a655903d103
--- /dev/null
+++ b/nixos/modules/services/networking/imaginary.nix
@@ -0,0 +1,113 @@
+{ lib, config, pkgs, utils, ... }:
+
+let
+  inherit (lib) mdDoc mkEnableOption mkIf mkOption types;
+
+  cfg = config.services.imaginary;
+in {
+  options.services.imaginary = {
+    enable = mkEnableOption (mdDoc "imaginary image processing microservice");
+
+    address = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = mdDoc ''
+        Bind address. Corresponds to the `-a` flag.
+        Set to `""` to bind to all addresses.
+      '';
+      example = "[::1]";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8088;
+      description = mdDoc "Bind port. Corresponds to the `-p` flag.";
+    };
+
+    settings = mkOption {
+      description = mdDoc ''
+        Command line arguments passed to the imaginary executable, stripped of
+        the prefix `-`. See upstream's
+        [README](https://github.com/h2non/imaginary#command-line-usage) for all
+        options.
+      '';
+      type = types.submodule {
+        freeformType = with types; attrsOf (oneOf [
+          bool
+          int
+          (nonEmptyListOf str)
+          str
+        ]);
+
+        options = {
+          return-size = mkOption {
+            type = types.bool;
+            default = false;
+            description = mdDoc "Return the image size in the HTTP headers.";
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [ {
+      assertion = ! lib.hasAttr "a" cfg.settings;
+      message = "Use services.imaginary.address to specify the -a flag.";
+    } {
+      assertion = ! lib.hasAttr "p" cfg.settings;
+      message = "Use services.imaginary.port to specify the -p flag.";
+    } ];
+
+    systemd.services.imaginary = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = rec {
+        ExecStart = let
+          args = lib.mapAttrsToList (key: val:
+            "-" + key + "=" + lib.concatStringsSep "," (map toString (lib.toList val))
+          ) (cfg.settings // { a = cfg.address; p = cfg.port; });
+        in "${pkgs.imaginary}/bin/imaginary ${utils.escapeSystemdExecArgs args}";
+        ProtectProc = "invisible";
+        BindReadOnlyPaths = lib.optional (cfg.settings ? mount) cfg.settings.mount;
+        CapabilityBoundingSet = if cfg.port < 1024 then
+          [ "CAP_NET_BIND_SERVICE" ]
+        else
+          [ "" ];
+        AmbientCapabilities = CapabilityBoundingSet;
+        NoNewPrivileges = true;
+        DynamicUser = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        TemporaryFileSystem = [ "/:ro" ];
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = cfg.port >= 1024;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+        ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+        ];
+        DevicePolicy = "closed";
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ dotlambda ];
+  };
+}
diff --git a/nixos/modules/services/networking/ircd-hybrid/builder.sh b/nixos/modules/services/networking/ircd-hybrid/builder.sh
index 38312210df2..d9d2e4264df 100644
--- a/nixos/modules/services/networking/ircd-hybrid/builder.sh
+++ b/nixos/modules/services/networking/ircd-hybrid/builder.sh
@@ -1,3 +1,4 @@
+if [ -e .attrs.sh ]; then source .attrs.sh; fi
 source $stdenv/setup
 
 doSub() {
diff --git a/nixos/modules/services/networking/iscsi/root-initiator.nix b/nixos/modules/services/networking/iscsi/root-initiator.nix
index 4434fedce1e..895467cc674 100644
--- a/nixos/modules/services/networking/iscsi/root-initiator.nix
+++ b/nixos/modules/services/networking/iscsi/root-initiator.nix
@@ -185,6 +185,10 @@ in
         assertion = cfg.loginAll -> cfg.target == null;
         message = "iSCSI target name is set while login on all portals is enabled.";
       }
+      {
+        assertion = !config.boot.initrd.systemd.enable;
+        message = "systemd stage 1 does not support iscsi yet.";
+      }
     ];
   };
 }
diff --git a/nixos/modules/services/networking/ivpn.nix b/nixos/modules/services/networking/ivpn.nix
new file mode 100644
index 00000000000..6df630c1f19
--- /dev/null
+++ b/nixos/modules/services/networking/ivpn.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.ivpn;
+in
+with lib;
+{
+  options.services.ivpn = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        This option enables iVPN daemon.
+        This sets {option}`networking.firewall.checkReversePath` to "loose", which might be undesirable for security.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "tun" ];
+
+    environment.systemPackages = with pkgs; [ ivpn ivpn-service ];
+
+    # iVPN writes to /etc/iproute2/rt_tables
+    networking.iproute2.enable = true;
+    networking.firewall.checkReversePath = "loose";
+
+    systemd.services.ivpn-service = {
+      description = "iVPN daemon";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network.target" ];
+      after = [
+        "network-online.target"
+        "NetworkManager.service"
+        "systemd-resolved.service"
+      ];
+      path = [
+        # Needed for mount
+        "/run/wrappers"
+      ];
+      startLimitBurst = 5;
+      startLimitIntervalSec = 20;
+      serviceConfig = {
+        ExecStart = "${pkgs.ivpn-service}/bin/ivpn-service --logging";
+        Restart = "always";
+        RestartSec = 1;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ ataraxiasjel ];
+}
diff --git a/nixos/modules/services/networking/jicofo.nix b/nixos/modules/services/networking/jicofo.nix
index 5e978896073..0886bbe004c 100644
--- a/nixos/modules/services/networking/jicofo.nix
+++ b/nixos/modules/services/networking/jicofo.nix
@@ -4,6 +4,15 @@ with lib;
 
 let
   cfg = config.services.jicofo;
+
+  # HOCON is a JSON superset that some jitsi-meet components use for configuration
+  toHOCON = x: if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}")
+    else if isAttrs x && x ? __hocon_unquoted_string then x.__hocon_unquoted_string
+    else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}"
+    else if isList x then "[${ concatMapStringsSep "," toHOCON x }]"
+    else builtins.toJSON x;
+
+  configFile = pkgs.writeText "jicofo.conf" (toHOCON cfg.config);
 in
 {
   options.services.jicofo = with types; {
@@ -68,22 +77,34 @@ in
     };
 
     config = mkOption {
-      type = attrsOf str;
+      type = (pkgs.formats.json {}).type;
       default = { };
       example = literalExpression ''
         {
-          "org.jitsi.jicofo.auth.URL" = "XMPP:jitsi-meet.example.com";
+          jicofo.bridge.max-bridge-participants = 42;
         }
       '';
       description = lib.mdDoc ''
-        Contents of the {file}`sip-communicator.properties` configuration file for jicofo.
+        Contents of the {file}`jicofo.conf` configuration file.
       '';
     };
   };
 
   config = mkIf cfg.enable {
-    services.jicofo.config = mapAttrs (_: v: mkDefault v) {
-      "org.jitsi.jicofo.BRIDGE_MUC" = cfg.bridgeMuc;
+    services.jicofo.config = {
+      jicofo = {
+        bridge.brewery-jid = cfg.bridgeMuc;
+        xmpp = rec {
+          client = {
+            hostname = cfg.xmppHost;
+            username = cfg.userName;
+            domain = cfg.userDomain;
+            password = { __hocon_envvar = "JICOFO_AUTH_PASS"; };
+            xmpp-domain = if cfg.xmppDomain == null then cfg.xmppHost else cfg.xmppDomain;
+          };
+          service = client;
+        };
+      };
     };
 
     users.groups.jitsi-meet = {};
@@ -93,6 +114,7 @@ in
         "-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION" = "/etc/jitsi";
         "-Dnet.java.sip.communicator.SC_HOME_DIR_NAME" = "jicofo";
         "-Djava.util.logging.config.file" = "/etc/jitsi/jicofo/logging.properties";
+        "-Dconfig.file" = configFile;
       };
     in
     {
@@ -101,18 +123,13 @@ in
       after = [ "network.target" ];
 
       restartTriggers = [
-        config.environment.etc."jitsi/jicofo/sip-communicator.properties".source
+        configFile
       ];
       environment.JAVA_SYS_PROPS = concatStringsSep " " (mapAttrsToList (k: v: "${k}=${toString v}") jicofoProps);
 
       script = ''
-        ${pkgs.jicofo}/bin/jicofo \
-          --host=${cfg.xmppHost} \
-          --domain=${if cfg.xmppDomain == null then cfg.xmppHost else cfg.xmppDomain} \
-          --secret=$(cat ${cfg.componentPasswordFile}) \
-          --user_name=${cfg.userName} \
-          --user_domain=${cfg.userDomain} \
-          --user_password=$(cat ${cfg.userPasswordFile})
+        export JICOFO_AUTH_PASS="$(<${cfg.userPasswordFile})"
+        exec "${pkgs.jicofo}/bin/jicofo"
       '';
 
       serviceConfig = {
@@ -140,10 +157,7 @@ in
       };
     };
 
-    environment.etc."jitsi/jicofo/sip-communicator.properties".source =
-      pkgs.writeText "sip-communicator.properties" (
-        generators.toKeyValue {} cfg.config
-      );
+    environment.etc."jitsi/jicofo/sip-communicator.properties".text = "";
     environment.etc."jitsi/jicofo/logging.properties".source =
       mkDefault "${pkgs.jicofo}/etc/jitsi/jicofo/logging.properties-journal";
   };
diff --git a/nixos/modules/services/networking/jitsi-videobridge.nix b/nixos/modules/services/networking/jitsi-videobridge.nix
index 09f2ddf92c5..37b0b1e5bf5 100644
--- a/nixos/modules/services/networking/jitsi-videobridge.nix
+++ b/nixos/modules/services/networking/jitsi-videobridge.nix
@@ -43,6 +43,7 @@ let
         muc_nickname = xmppConfig.mucNickname;
         disable_certificate_verification = xmppConfig.disableCertificateVerification;
       });
+      apis.rest.enabled = cfg.colibriRestApi;
     };
   };
 
@@ -50,6 +51,11 @@ let
   jvbConfig = recursiveUpdate defaultJvbConfig cfg.config;
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "jitsi-videobridge" "apis" ]
+      "services.jitsi-videobridge.apis was broken and has been migrated into the boolean option services.jitsi-videobridge.colibriRestApi. It is set to false by default, setting it to true will correctly enable the private /colibri rest API."
+    )
+  ];
   options.services.jitsi-videobridge = with types; {
     enable = mkEnableOption (lib.mdDoc "Jitsi Videobridge, a WebRTC compatible video router");
 
@@ -192,14 +198,13 @@ in
       '';
     };
 
-    apis = mkOption {
-      type = with types; listOf str;
+    colibriRestApi = mkOption {
+      type = bool;
       description = lib.mdDoc ''
-        What is passed as --apis= parameter. If this is empty, "none" is passed.
-        Needed for monitoring jitsi.
+        Whether to enable the private rest API for the COLIBRI control interface.
+        Needed for monitoring jitsi, enabling scraping of the /colibri/stats endpoint.
       '';
-      default = [];
-      example = literalExpression "[ \"colibri\" \"rest\" ]";
+      default = false;
     };
   };
 
@@ -233,7 +238,7 @@ in
         "export ${toVarName name}=$(cat ${xmppConfig.passwordFile})\n"
       ) cfg.xmppConfigs))
       + ''
-        ${pkgs.jitsi-videobridge}/bin/jitsi-videobridge --apis=${if (cfg.apis == []) then "none" else concatStringsSep "," cfg.apis}
+        ${pkgs.jitsi-videobridge}/bin/jitsi-videobridge
       '';
 
       serviceConfig = {
diff --git a/nixos/modules/services/networking/kresd.nix b/nixos/modules/services/networking/kresd.nix
index 55af6abd5e0..3ad757133a6 100644
--- a/nixos/modules/services/networking/kresd.nix
+++ b/nixos/modules/services/networking/kresd.nix
@@ -79,7 +79,7 @@ in {
       example = [ "53" ];
       description = lib.mdDoc ''
         What addresses and ports the server should listen on.
-        For detailed syntax see ListenStream in man systemd.socket.
+        For detailed syntax see ListenStream in {manpage}`systemd.socket(5)`.
       '';
     };
     listenTLS = mkOption {
@@ -88,7 +88,7 @@ in {
       example = [ "198.51.100.1:853" "[2001:db8::1]:853" "853" ];
       description = lib.mdDoc ''
         Addresses and ports on which kresd should provide DNS over TLS (see RFC 7858).
-        For detailed syntax see ListenStream in man systemd.socket.
+        For detailed syntax see ListenStream in {manpage}`systemd.socket(5)`.
       '';
     };
     listenDoH = mkOption {
@@ -97,7 +97,7 @@ in {
       example = [ "198.51.100.1:443" "[2001:db8::1]:443" "443" ];
       description = lib.mdDoc ''
         Addresses and ports on which kresd should provide DNS over HTTPS/2 (see RFC 8484).
-        For detailed syntax see ListenStream in man systemd.socket.
+        For detailed syntax see ListenStream in {manpage}`systemd.socket(5)`.
       '';
     };
     instances = mkOption {
diff --git a/nixos/modules/services/networking/legit.nix b/nixos/modules/services/networking/legit.nix
new file mode 100644
index 00000000000..90234f3955e
--- /dev/null
+++ b/nixos/modules/services/networking/legit.nix
@@ -0,0 +1,182 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib)
+    literalExpression
+    mkEnableOption
+    mdDoc
+    mkIf
+    mkOption
+    mkPackageOptionMD
+    optionalAttrs
+    optional
+    types;
+
+  cfg = config.services.legit;
+
+  yaml = pkgs.formats.yaml { };
+  configFile = yaml.generate "legit.yaml" cfg.settings;
+
+  defaultStateDir = "/var/lib/legit";
+  defaultStaticDir = "${cfg.settings.repo.scanPath}/static";
+  defaultTemplatesDir = "${cfg.settings.repo.scanPath}/templates";
+in
+{
+  options.services.legit = {
+    enable = mkEnableOption (mdDoc "legit git web frontend");
+
+    package = mkPackageOptionMD pkgs "legit-web" { };
+
+    user = mkOption {
+      type = types.str;
+      default = "legit";
+      description = mdDoc "User account under which legit runs.";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "legit";
+      description = mdDoc "Group account under which legit runs.";
+    };
+
+    settings = mkOption {
+      default = { };
+      description = mdDoc ''
+        The primary legit configuration. See the
+        [sample configuration](https://github.com/icyphox/legit/blob/master/config.yaml)
+        for possible values.
+      '';
+      type = types.submodule {
+        options.repo = {
+          scanPath = mkOption {
+            type = types.path;
+            default = defaultStateDir;
+            description = mdDoc "Directory where legit will scan for repositories.";
+          };
+          readme = mkOption {
+            type = types.listOf types.str;
+            default = [ ];
+            description = mdDoc "Readme files to look for.";
+          };
+          mainBranch = mkOption {
+            type = types.listOf types.str;
+            default = [ "main" "master" ];
+            description = mdDoc "Main branch to look for.";
+          };
+          ignore = mkOption {
+            type = types.listOf types.str;
+            default = [ ];
+            description = mdDoc "Repositories to ignore.";
+          };
+        };
+        options.dirs = {
+          templates = mkOption {
+            type = types.path;
+            default = "${pkgs.legit-web}/lib/legit/templates";
+            defaultText = literalExpression ''"''${pkgs.legit-web}/lib/legit/templates"'';
+            description = mdDoc "Directories where template files are located.";
+          };
+          static = mkOption {
+            type = types.path;
+            default = "${pkgs.legit-web}/lib/legit/static";
+            defaultText = literalExpression ''"''${pkgs.legit-web}/lib/legit/static"'';
+            description = mdDoc "Directories where static files are located.";
+          };
+        };
+        options.meta = {
+          title = mkOption {
+            type = types.str;
+            default = "legit";
+            description = mdDoc "Website title.";
+          };
+          description = mkOption {
+            type = types.str;
+            default = "git frontend";
+            description = mdDoc "Website description.";
+          };
+        };
+        options.server = {
+          name = mkOption {
+            type = types.str;
+            default = "localhost";
+            description = mdDoc "Server name.";
+          };
+          host = mkOption {
+            type = types.str;
+            default = "127.0.0.1";
+            description = mdDoc "Host address.";
+          };
+          port = mkOption {
+            type = types.port;
+            default = 5555;
+            description = mdDoc "Legit port.";
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups = optionalAttrs (cfg.group == "legit") {
+      "${cfg.group}" = { };
+    };
+
+    users.users = optionalAttrs (cfg.user == "legit") {
+      "${cfg.user}" = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    systemd.services.legit = {
+      description = "legit git frontend";
+
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ configFile ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/legit -config ${configFile}";
+        Restart = "always";
+
+        WorkingDirectory = cfg.settings.repo.scanPath;
+        StateDirectory = [ ] ++
+          optional (cfg.settings.repo.scanPath == defaultStateDir) "legit" ++
+          optional (cfg.settings.dirs.static == defaultStaticDir) "legit/static" ++
+          optional (cfg.settings.dirs.templates == defaultTemplatesDir) "legit/templates";
+
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DeviceAllow = [ "" ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        ReadWritePaths = cfg.settings.repo.scanPath;
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
+        UMask = "0077";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/minidlna.nix b/nixos/modules/services/networking/minidlna.nix
index 549f1fe5de3..d0de6cd4fdc 100644
--- a/nixos/modules/services/networking/minidlna.nix
+++ b/nixos/modules/services/networking/minidlna.nix
@@ -16,7 +16,7 @@ in
     description = lib.mdDoc ''
       Whether to enable MiniDLNA, a simple DLNA server.
       It serves media files such as video and music to DLNA client devices
-      such as televisions and media players. If you use the firewall consider
+      such as televisions and media players. If you use the firewall, consider
       adding the following: `services.minidlna.openFirewall = true;`
     '';
   };
@@ -54,10 +54,7 @@ in
         description = lib.mdDoc ''
           The interval between announces (in seconds).
           Instead of waiting for announces, you should set `openFirewall` option to use SSDP discovery.
-          Furthermore, this option has been set to 90000 in order to prevent disconnects with certain
-          clients and relies solely on the discovery.
-
-          Lower values (e.g. 30 seconds) should be used if you can't use the discovery.
+          Lower values (e.g. 30 seconds) should be used if your network blocks the discovery unicast.
           Some relevant information can be found here:
           https://sourceforge.net/p/minidlna/discussion/879957/thread/1389d197/
         '';
@@ -82,8 +79,8 @@ in
       };
       options.root_container = mkOption {
         type = types.str;
-        default = ".";
-        example = "B";
+        default = "B";
+        example = ".";
         description = lib.mdDoc "Use a different container as the root of the directory tree presented to clients.";
       };
       options.log_level = mkOption {
@@ -133,22 +130,19 @@ in
 
     users.groups.minidlna.gid = config.ids.gids.minidlna;
 
-    systemd.services.minidlna =
-      { description = "MiniDLNA Server";
-
-        wantedBy = [ "multi-user.target" ];
-        after = [ "network.target" ];
+    systemd.services.minidlna = {
+      description = "MiniDLNA Server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
 
-        serviceConfig =
-          { User = "minidlna";
-            Group = "minidlna";
-            CacheDirectory = "minidlna";
-            RuntimeDirectory = "minidlna";
-            PIDFile = "/run/minidlna/pid";
-            ExecStart =
-              "${pkgs.minidlna}/sbin/minidlnad -S -P /run/minidlna/pid" +
-              " -f ${settingsFile}";
-          };
+      serviceConfig = {
+        User = "minidlna";
+        Group = "minidlna";
+        CacheDirectory = "minidlna";
+        RuntimeDirectory = "minidlna";
+        PIDFile = "/run/minidlna/pid";
+        ExecStart = "${pkgs.minidlna}/sbin/minidlnad -S -P /run/minidlna/pid -f ${settingsFile}";
       };
+    };
   };
 }
diff --git a/nixos/modules/services/networking/mosquitto.nix b/nixos/modules/services/networking/mosquitto.nix
index 270450cb0c6..a4fd2fd7c89 100644
--- a/nixos/modules/services/networking/mosquitto.nix
+++ b/nixos/modules/services/networking/mosquitto.nix
@@ -671,8 +671,6 @@ in
 
   meta = {
     maintainers = with lib.maintainers; [ pennae ];
-    # Don't edit the docbook xml directly, edit the md and generate it:
-    # `pandoc mosquitto.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > mosquitto.xml`
-    doc = ./mosquitto.xml;
+    doc = ./mosquitto.md;
   };
 }
diff --git a/nixos/modules/services/networking/mosquitto.xml b/nixos/modules/services/networking/mosquitto.xml
deleted file mode 100644
index d16ab28c026..00000000000
--- a/nixos/modules/services/networking/mosquitto.xml
+++ /dev/null
@@ -1,147 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-mosquitto">
-  <title>Mosquitto</title>
-  <para>
-    Mosquitto is a MQTT broker often used for IoT or home automation
-    data transport.
-  </para>
-  <section xml:id="module-services-mosquitto-quickstart">
-    <title>Quickstart</title>
-    <para>
-      A minimal configuration for Mosquitto is
-    </para>
-    <programlisting language="bash">
-services.mosquitto = {
-  enable = true;
-  listeners = [ {
-    acl = [ &quot;pattern readwrite #&quot; ];
-    omitPasswordAuth = true;
-    settings.allow_anonymous = true;
-  } ];
-};
-</programlisting>
-    <para>
-      This will start a broker on port 1883, listening on all interfaces
-      of the machine, allowing read/write access to all topics to any
-      user without password requirements.
-    </para>
-    <para>
-      User authentication can be configured with the
-      <literal>users</literal> key of listeners. A config that gives
-      full read access to a user <literal>monitor</literal> and
-      restricted write access to a user <literal>service</literal> could
-      look like
-    </para>
-    <programlisting language="bash">
-services.mosquitto = {
-  enable = true;
-  listeners = [ {
-    users = {
-      monitor = {
-        acl = [ &quot;read #&quot; ];
-        password = &quot;monitor&quot;;
-      };
-      service = {
-        acl = [ &quot;write service/#&quot; ];
-        password = &quot;service&quot;;
-      };
-    };
-  } ];
-};
-</programlisting>
-    <para>
-      TLS authentication is configured by setting TLS-related options of
-      the listener:
-    </para>
-    <programlisting language="bash">
-services.mosquitto = {
-  enable = true;
-  listeners = [ {
-    port = 8883; # port change is not required, but helpful to avoid mistakes
-    # ...
-    settings = {
-      cafile = &quot;/path/to/mqtt.ca.pem&quot;;
-      certfile = &quot;/path/to/mqtt.pem&quot;;
-      keyfile = &quot;/path/to/mqtt.key&quot;;
-    };
-  } ];
-</programlisting>
-  </section>
-  <section xml:id="module-services-mosquitto-config">
-    <title>Configuration</title>
-    <para>
-      The Mosquitto configuration has four distinct types of settings:
-      the global settings of the daemon, listeners, plugins, and
-      bridges. Bridges and listeners are part of the global
-      configuration, plugins are part of listeners. Users of the broker
-      are configured as parts of listeners rather than globally,
-      allowing configurations in which a given user is only allowed to
-      log in to the broker using specific listeners (eg to configure an
-      admin user with full access to all topics, but restricted to
-      localhost).
-    </para>
-    <para>
-      Almost all options of Mosquitto are available for configuration at
-      their appropriate levels, some as NixOS options written in camel
-      case, the remainders under <literal>settings</literal> with their
-      exact names in the Mosquitto config file. The exceptions are
-      <literal>acl_file</literal> (which is always set according to the
-      <literal>acl</literal> attributes of a listener and its users) and
-      <literal>per_listener_settings</literal> (which is always set to
-      <literal>true</literal>).
-    </para>
-    <section xml:id="module-services-mosquitto-config-passwords">
-      <title>Password authentication</title>
-      <para>
-        Mosquitto can be run in two modes, with a password file or
-        without. Each listener has its own password file, and different
-        listeners may use different password files. Password file
-        generation can be disabled by setting
-        <literal>omitPasswordAuth = true</literal> for a listener; in
-        this case it is necessary to either set
-        <literal>settings.allow_anonymous = true</literal> to allow all
-        logins, or to configure other authentication methods like TLS
-        client certificates with
-        <literal>settings.use_identity_as_username = true</literal>.
-      </para>
-      <para>
-        The default is to generate a password file for each listener
-        from the users configured to that listener. Users with no
-        configured password will not be added to the password file and
-        thus will not be able to use the broker.
-      </para>
-    </section>
-    <section xml:id="module-services-mosquitto-config-acl">
-      <title>ACL format</title>
-      <para>
-        Every listener has a Mosquitto <literal>acl_file</literal>
-        attached to it. This ACL is configured via two attributes of the
-        config:
-      </para>
-      <itemizedlist spacing="compact">
-        <listitem>
-          <para>
-            the <literal>acl</literal> attribute of the listener
-            configures pattern ACL entries and topic ACL entries for
-            anonymous users. Each entry must be prefixed with
-            <literal>pattern</literal> or <literal>topic</literal> to
-            distinguish between these two cases.
-          </para>
-        </listitem>
-        <listitem>
-          <para>
-            the <literal>acl</literal> attribute of every user
-            configures in the listener configured the ACL for that given
-            user. Only topic ACLs are supported by Mosquitto in this
-            setting, so no prefix is required or allowed.
-          </para>
-        </listitem>
-      </itemizedlist>
-      <para>
-        The default ACL for a listener is empty, disallowing all
-        accesses from all clients. To configure a completely open ACL,
-        set <literal>acl = [ &quot;pattern readwrite #&quot; ]</literal>
-        in the listener.
-      </para>
-    </section>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/networking/multipath.nix b/nixos/modules/services/networking/multipath.nix
index 54ee2a01568..bd403e109c2 100644
--- a/nixos/modules/services/networking/multipath.nix
+++ b/nixos/modules/services/networking/multipath.nix
@@ -513,23 +513,22 @@ in {
         ${indentLines 2 devices}
         }
 
-        ${optionalString (!isNull defaults) ''
+        ${optionalString (defaults != null) ''
           defaults {
           ${indentLines 2 defaults}
-            multipath_dir ${cfg.package}/lib/multipath
           }
         ''}
-        ${optionalString (!isNull blacklist) ''
+        ${optionalString (blacklist != null) ''
           blacklist {
           ${indentLines 2 blacklist}
           }
         ''}
-        ${optionalString (!isNull blacklist_exceptions) ''
+        ${optionalString (blacklist_exceptions != null) ''
           blacklist_exceptions {
           ${indentLines 2 blacklist_exceptions}
           }
         ''}
-        ${optionalString (!isNull overrides) ''
+        ${optionalString (overrides != null) ''
           overrides {
           ${indentLines 2 overrides}
           }
diff --git a/nixos/modules/services/networking/murmur.nix b/nixos/modules/services/networking/murmur.nix
index 32498ca25ea..ebade7aa8e4 100644
--- a/nixos/modules/services/networking/murmur.nix
+++ b/nixos/modules/services/networking/murmur.nix
@@ -42,6 +42,8 @@ let
     ${if cfg.sslKey  == "" then "" else "sslKey="+cfg.sslKey}
     ${if cfg.sslCa   == "" then "" else "sslCA="+cfg.sslCa}
 
+    ${lib.optionalString (cfg.dbus != null) "dbus=${cfg.dbus}"}
+
     ${cfg.extraConfig}
   '';
 in
@@ -282,6 +284,12 @@ in
           `murmur` is running.
         '';
       };
+
+      dbus = mkOption {
+        type = types.enum [ null "session" "system" ];
+        default = null;
+        description = lib.mdDoc "Enable D-Bus remote control. Set to the bus you want Murmur to connect to.";
+      };
     };
   };
 
@@ -305,7 +313,7 @@ in
     systemd.services.murmur = {
       description = "Murmur Chat Service";
       wantedBy    = [ "multi-user.target" ];
-      after       = [ "network-online.target" ];
+      after       = [ "network.target" ];
       preStart    = ''
         ${pkgs.envsubst}/bin/envsubst \
           -o /run/murmur/murmurd.ini \
@@ -325,5 +333,27 @@ in
         Group = "murmur";
       };
     };
+
+    # currently not included in upstream package, addition requested at
+    # https://github.com/mumble-voip/mumble/issues/6078
+    services.dbus.packages = mkIf (cfg.dbus == "system") [(pkgs.writeTextFile {
+      name = "murmur-dbus-policy";
+      text = ''
+        <!DOCTYPE busconfig PUBLIC
+          "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+          "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+        <busconfig>
+          <policy user="murmur">
+            <allow own="net.sourceforge.mumble.murmur"/>
+          </policy>
+
+          <policy context="default">
+            <allow send_destination="net.sourceforge.mumble.murmur"/>
+            <allow receive_sender="net.sourceforge.mumble.murmur"/>
+          </policy>
+        </busconfig>
+      '';
+      destination = "/share/dbus-1/system.d/murmur.conf";
+    })];
   };
 }
diff --git a/nixos/modules/services/networking/nat.nix b/nixos/modules/services/networking/nat.nix
index a6f403b46f8..3afe6fe0a97 100644
--- a/nixos/modules/services/networking/nat.nix
+++ b/nixos/modules/services/networking/nat.nix
@@ -124,7 +124,7 @@ in
             type = types.listOf types.str;
             default = [ ];
             example = literalExpression ''[ "55.1.2.3" ]'';
-            description = lib.mdDoc "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort' from the host itself and from other hosts behind NAT";
+            description = lib.mdDoc "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort` from the host itself and from other hosts behind NAT";
           };
         };
       });
diff --git a/nixos/modules/services/networking/ndppd.nix b/nixos/modules/services/networking/ndppd.nix
index 98c58d2d5db..d221c95ae62 100644
--- a/nixos/modules/services/networking/ndppd.nix
+++ b/nixos/modules/services/networking/ndppd.nix
@@ -17,7 +17,7 @@ let
       ttl ${toString proxy.ttl}
       ${render proxy.rules (ruleNetworkName: rule: ''
       rule ${prefer rule.network ruleNetworkName} {
-        ${rule.method}${if rule.method == "iface" then " ${rule.interface}" else ""}
+        ${rule.method}${optionalString (rule.method == "iface") " ${rule.interface}"}
       }'')}
     }'')}
   '');
diff --git a/nixos/modules/services/networking/nebula.nix b/nixos/modules/services/networking/nebula.nix
index 2bedafc5d9f..e1a8c6740f5 100644
--- a/nixos/modules/services/networking/nebula.nix
+++ b/nixos/modules/services/networking/nebula.nix
@@ -68,6 +68,12 @@ in
               description = lib.mdDoc "Whether this node is a lighthouse.";
             };
 
+            isRelay = mkOption {
+              type = types.bool;
+              default = false;
+              description = lib.mdDoc "Whether this node is a relay.";
+            };
+
             lighthouses = mkOption {
               type = types.listOf types.str;
               default = [];
@@ -78,6 +84,15 @@ in
               example = [ "192.168.100.1" ];
             };
 
+            relays = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = lib.mdDoc ''
+                List of IPs of relays that this node should allow traffic from.
+              '';
+              example = [ "192.168.100.1" ];
+            };
+
             listen.host = mkOption {
               type = types.str;
               default = "0.0.0.0";
@@ -157,6 +172,11 @@ in
             am_lighthouse = netCfg.isLighthouse;
             hosts = netCfg.lighthouses;
           };
+          relay = {
+            am_relay = netCfg.isRelay;
+            relays = netCfg.relays;
+            use_relays = true;
+          };
           listen = {
             host = netCfg.listen.host;
             port = netCfg.listen.port;
@@ -173,25 +193,41 @@ in
         configFile = format.generate "nebula-config-${netName}.yml" settings;
         in
         {
-          # Create systemd service for Nebula.
+          # Create the systemd service for Nebula.
           "nebula@${netName}" = {
             description = "Nebula VPN service for ${netName}";
             wants = [ "basic.target" ];
             after = [ "basic.target" "network.target" ];
             before = [ "sshd.service" ];
             wantedBy = [ "multi-user.target" ];
-            serviceConfig = mkMerge [
-              {
-                Type = "simple";
-                Restart = "always";
-                ExecStart = "${netCfg.package}/bin/nebula -config ${configFile}";
-              }
-              # The service needs to launch as root to access the tun device, if it's enabled.
-              (mkIf netCfg.tun.disable {
-                User = networkId;
-                Group = networkId;
-              })
-            ];
+            serviceConfig = {
+              Type = "simple";
+              Restart = "always";
+              ExecStart = "${netCfg.package}/bin/nebula -config ${configFile}";
+              UMask = "0027";
+              CapabilityBoundingSet = "CAP_NET_ADMIN";
+              AmbientCapabilities = "CAP_NET_ADMIN";
+              LockPersonality = true;
+              NoNewPrivileges = true;
+              PrivateDevices = false; # needs access to /dev/net/tun (below)
+              DeviceAllow = "/dev/net/tun rw";
+              DevicePolicy = "closed";
+              PrivateTmp = true;
+              PrivateUsers = false; # CapabilityBoundingSet needs to apply to the host namespace
+              ProtectClock = true;
+              ProtectControlGroups = true;
+              ProtectHome = true;
+              ProtectHostname = true;
+              ProtectKernelLogs = true;
+              ProtectKernelModules = true;
+              ProtectKernelTunables = true;
+              ProtectProc = "invisible";
+              ProtectSystem = "strict";
+              RestrictNamespaces = true;
+              RestrictSUIDSGID = true;
+              User = networkId;
+              Group = networkId;
+            };
             unitConfig.StartLimitIntervalSec = 0; # ensure Restart=always is always honoured (networks can go down for arbitrarily long)
           };
         }) enabledNetworks);
@@ -202,7 +238,7 @@ in
 
     # Create the service users and groups.
     users.users = mkMerge (mapAttrsToList (netName: netCfg:
-      mkIf netCfg.tun.disable {
+      {
         ${nameToId netName} = {
           group = nameToId netName;
           description = "Nebula service user for network ${netName}";
@@ -210,9 +246,8 @@ in
         };
       }) enabledNetworks);
 
-    users.groups = mkMerge (mapAttrsToList (netName: netCfg:
-      mkIf netCfg.tun.disable {
-        ${nameToId netName} = {};
-      }) enabledNetworks);
+    users.groups = mkMerge (mapAttrsToList (netName: netCfg: {
+      ${nameToId netName} = {};
+    }) enabledNetworks);
   };
 }
diff --git a/nixos/modules/services/networking/netbird.nix b/nixos/modules/services/networking/netbird.nix
index 5bd9e9ca616..647c0ce3e6d 100644
--- a/nixos/modules/services/networking/netbird.nix
+++ b/nixos/modules/services/networking/netbird.nix
@@ -41,9 +41,10 @@ in {
       documentation = [ "https://netbird.io/docs/" ];
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [
+        openresolv
+      ];
       serviceConfig = {
-        AmbientCapabilities = [ "CAP_NET_ADMIN" ];
-        DynamicUser = true;
         Environment = [
           "NB_CONFIG=/var/lib/netbird/config.json"
           "NB_LOG_FILE=console"
diff --git a/nixos/modules/services/networking/networkd-dispatcher.nix b/nixos/modules/services/networking/networkd-dispatcher.nix
new file mode 100644
index 00000000000..c5319ca7b88
--- /dev/null
+++ b/nixos/modules/services/networking/networkd-dispatcher.nix
@@ -0,0 +1,98 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.networkd-dispatcher;
+
+in {
+
+  options = {
+    services.networkd-dispatcher = {
+
+      enable = mkEnableOption (mdDoc ''
+        Networkd-dispatcher service for systemd-networkd connection status
+        change. See [https://gitlab.com/craftyguy/networkd-dispatcher](upstream instructions)
+        for usage.
+      '');
+
+      rules = mkOption {
+        default = {};
+        example = lib.literalExpression ''
+          { "restart-tor" = {
+              onState = ["routable" "off"];
+              script = '''
+                #!''${pkgs.runtimeShell}
+                if [[ $IFACE == "wlan0" && $AdministrativeState == "configured" ]]; then
+                  echo "Restarting Tor ..."
+                  systemctl restart tor
+                fi
+                exit 0
+              ''';
+            };
+          };
+        '';
+        description = lib.mdDoc ''
+          Declarative configuration of networkd-dispatcher rules. See
+          [https://gitlab.com/craftyguy/networkd-dispatcher](upstream instructions)
+          for an introduction and example scripts.
+        '';
+        type = types.attrsOf (types.submodule {
+          options = {
+            onState = mkOption {
+              type = types.listOf (types.enum [
+                "routable" "dormant" "no-carrier" "off" "carrier" "degraded"
+                "configuring" "configured"
+              ]);
+              default = null;
+              description = lib.mdDoc ''
+                List of names of the systemd-networkd operational states which
+                should trigger the script. See <https://www.freedesktop.org/software/systemd/man/networkctl.html>
+                for a description of the specific state type.
+              '';
+            };
+            script = mkOption {
+              type = types.lines;
+              description = lib.mdDoc ''
+                Shell commands executed on specified operational states.
+              '';
+            };
+          };
+        });
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd = {
+      packages = [ pkgs.networkd-dispatcher ];
+      services.networkd-dispatcher = {
+        wantedBy = [ "multi-user.target" ];
+        # Override existing ExecStart definition
+        serviceConfig.ExecStart = let
+          scriptDir = pkgs.symlinkJoin {
+            name = "networkd-dispatcher-script-dir";
+            paths = lib.mapAttrsToList (name: cfg:
+              (map(state:
+                pkgs.writeTextFile {
+                  inherit name;
+                  text = cfg.script;
+                  destination = "/${state}.d/${name}";
+                  executable = true;
+                }
+              ) cfg.onState)
+            ) cfg.rules;
+          };
+        in [
+          ""
+          "${pkgs.networkd-dispatcher}/bin/networkd-dispatcher -v --script-dir ${scriptDir} $networkd_dispatcher_args"
+        ];
+      };
+    };
+
+  };
+}
+
diff --git a/nixos/modules/services/networking/nftables.nix b/nixos/modules/services/networking/nftables.nix
index bd13e8c9929..faff1dca89b 100644
--- a/nixos/modules/services/networking/nftables.nix
+++ b/nixos/modules/services/networking/nftables.nix
@@ -28,6 +28,32 @@ in
           <https://wiki.nftables.org/wiki-nftables/index.php/Troubleshooting#Question_4._How_do_nftables_and_iptables_interact_when_used_on_the_same_system.3F>.
         '';
     };
+
+    networking.nftables.checkRuleset = mkOption {
+      type = types.bool;
+      default = true;
+      description = lib.mdDoc ''
+        Run `nft check` on the ruleset to spot syntax errors during build.
+        Because this is executed in a sandbox, the check might fail if it requires
+        access to any environmental factors or paths outside the Nix store.
+        To circumvent this, the ruleset file can be edited using the preCheckRuleset
+        option to work in the sandbox environment.
+      '';
+    };
+
+    networking.nftables.preCheckRuleset = mkOption {
+      type = types.lines;
+      default = "";
+      example = lib.literalExpression ''
+        sed 's/skgid meadow/skgid nogroup/g' -i ruleset.conf
+      '';
+      description = lib.mdDoc ''
+        This script gets run before the ruleset is checked. It can be used to
+        create additional files needed for the ruleset check to work, or modify
+        the ruleset for cases the build environment cannot cover.
+      '';
+    };
+
     networking.nftables.ruleset = mkOption {
       type = types.lines;
       default = "";
@@ -105,13 +131,24 @@ in
       wantedBy = [ "multi-user.target" ];
       reloadIfChanged = true;
       serviceConfig = let
-        rulesScript = pkgs.writeScript "nftables-rules" ''
-          #! ${pkgs.nftables}/bin/nft -f
-          flush ruleset
-          ${if cfg.rulesetFile != null then ''
-            include "${cfg.rulesetFile}"
-          '' else cfg.ruleset}
-        '';
+        rulesScript = pkgs.writeTextFile {
+          name =  "nftables-rules";
+          executable = true;
+          text = ''
+            #! ${pkgs.nftables}/bin/nft -f
+            flush ruleset
+            ${if cfg.rulesetFile != null then ''
+              include "${cfg.rulesetFile}"
+            '' else cfg.ruleset}
+          '';
+          checkPhase = lib.optionalString cfg.checkRuleset ''
+            cp $out ruleset.conf
+            ${cfg.preCheckRuleset}
+            export NIX_REDIRECTS=/etc/protocols=${pkgs.buildPackages.iana-etc}/etc/protocols:/etc/services=${pkgs.buildPackages.iana-etc}/etc/services
+            LD_PRELOAD="${pkgs.buildPackages.libredirect}/lib/libredirect.so ${pkgs.buildPackages.lklWithFirewall.lib}/lib/liblkl-hijack.so" \
+              ${pkgs.buildPackages.nftables}/bin/nft --check --file ruleset.conf
+          '';
+        };
       in {
         Type = "oneshot";
         RemainAfterExit = true;
diff --git a/nixos/modules/services/networking/nomad.nix b/nixos/modules/services/networking/nomad.nix
index 890ee0b7d8d..b1e51195247 100644
--- a/nixos/modules/services/networking/nomad.nix
+++ b/nixos/modules/services/networking/nomad.nix
@@ -67,10 +67,21 @@ in
           Additional plugins dir used to configure nomad.
         '';
         example = literalExpression ''
-          [ "<pluginDir>" pkgs.<plugins-name> ]
+          [ "<pluginDir>" pkgs.nomad-driver-nix pkgs.nomad-driver-podman  ]
         '';
       };
 
+      credentials = mkOption {
+        description = lib.mdDoc ''
+          Credentials envs used to configure nomad secrets.
+        '';
+        type = types.attrsOf types.str;
+        default = { };
+
+        example = {
+          logs_remote_write_password = "/run/keys/nomad_write_password";
+        };
+      };
 
       settings = mkOption {
         type = format.type;
@@ -139,9 +150,17 @@ in
         {
           DynamicUser = cfg.dropPrivileges;
           ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-          ExecStart = "${cfg.package}/bin/nomad agent -config=/etc/nomad.json" +
+          ExecStart =
+            let
+              pluginsDir = pkgs.symlinkJoin
+                {
+                  name = "nomad-plugins";
+                  paths = cfg.extraSettingsPlugins;
+                };
+            in
+            "${cfg.package}/bin/nomad agent -config=/etc/nomad.json -plugin-dir=${pluginsDir}/bin" +
             concatMapStrings (path: " -config=${path}") cfg.extraSettingsPaths +
-            concatMapStrings (path: " -plugin-dir=${path}/bin") cfg.extraSettingsPlugins;
+            concatMapStrings (key: " -config=\${CREDENTIALS_DIRECTORY}/${key}") (lib.attrNames cfg.credentials);
           KillMode = "process";
           KillSignal = "SIGINT";
           LimitNOFILE = 65536;
@@ -150,6 +169,7 @@ in
           Restart = "on-failure";
           RestartSec = 2;
           TasksMax = "infinity";
+          LoadCredential = lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials;
         }
         (mkIf cfg.enableDocker {
           SupplementaryGroups = "docker"; # space-separated string
diff --git a/nixos/modules/services/networking/ntopng.nix b/nixos/modules/services/networking/ntopng.nix
index e6344d7ff3b..bf7ec19f02a 100644
--- a/nixos/modules/services/networking/ntopng.nix
+++ b/nixos/modules/services/networking/ntopng.nix
@@ -86,7 +86,7 @@ in
 
       redis.createInstance = mkOption {
         type = types.nullOr types.str;
-        default = if versionAtLeast config.system.stateVersion "22.05" then "ntopng" else "";
+        default = optionalString (versionAtLeast config.system.stateVersion "22.05") "ntopng";
         description = lib.mdDoc ''
           Local Redis instance name. Set to `null` to disable
           local Redis instance. Defaults to `""` for
diff --git a/nixos/modules/services/networking/ntp/chrony.nix b/nixos/modules/services/networking/ntp/chrony.nix
index dc180d4a4f9..2d421abc8be 100644
--- a/nixos/modules/services/networking/ntp/chrony.nix
+++ b/nixos/modules/services/networking/ntp/chrony.nix
@@ -185,7 +185,7 @@ in
           ProtectSystem = "full";
           ProtectHome = true;
           PrivateTmp = true;
-          PrivateDevices = true;
+          PrivateDevices = false;
           PrivateUsers = false;
           ProtectHostname = true;
           ProtectClock = false;
@@ -203,7 +203,7 @@ in
           PrivateMounts = true;
           # System Call Filtering
           SystemCallArchitectures = "native";
-          SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @resources" "@clock" "@setuid" "capset" "chown" ];
+          SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @resources" "@clock" "@setuid" "capset" "@chown" ];
         };
       };
   };
diff --git a/nixos/modules/services/networking/openconnect.nix b/nixos/modules/services/networking/openconnect.nix
index 4676b1733af..7f9006053b8 100644
--- a/nixos/modules/services/networking/openconnect.nix
+++ b/nixos/modules/services/networking/openconnect.nix
@@ -90,6 +90,7 @@ let
   generateConfig = name: icfg:
     pkgs.writeText "config" ''
       interface=${name}
+      ${optionalString (icfg.protocol != null) "protocol=${icfg.protocol}"}
       ${optionalString (icfg.user != null) "user=${icfg.user}"}
       ${optionalString (icfg.passwordFile != null) "passwd-on-stdin"}
       ${optionalString (icfg.certificate != null)
@@ -116,7 +117,7 @@ let
   };
 in {
   options.networking.openconnect = {
-    package = mkPackageOption pkgs "openconnect" { };
+    package = mkPackageOptionMD pkgs "openconnect" { };
 
     interfaces = mkOption {
       description = lib.mdDoc "OpenConnect interfaces.";
diff --git a/nixos/modules/services/networking/openvpn.nix b/nixos/modules/services/networking/openvpn.nix
index 492a0936fdb..9a5866f2afd 100644
--- a/nixos/modules/services/networking/openvpn.nix
+++ b/nixos/modules/services/networking/openvpn.nix
@@ -14,7 +14,6 @@ let
       path = makeBinPath (getAttr "openvpn-${name}" config.systemd.services).path;
 
       upScript = ''
-        #! /bin/sh
         export PATH=${path}
 
         # For convenience in client scripts, extract the remote domain
@@ -34,7 +33,6 @@ let
       '';
 
       downScript = ''
-        #! /bin/sh
         export PATH=${path}
         ${optionalString cfg.updateResolvConf
            "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
@@ -47,9 +45,9 @@ let
           ${optionalString (cfg.up != "" || cfg.down != "" || cfg.updateResolvConf) "script-security 2"}
           ${cfg.config}
           ${optionalString (cfg.up != "" || cfg.updateResolvConf)
-              "up ${pkgs.writeScript "openvpn-${name}-up" upScript}"}
+              "up ${pkgs.writeShellScript "openvpn-${name}-up" upScript}"}
           ${optionalString (cfg.down != "" || cfg.updateResolvConf)
-              "down ${pkgs.writeScript "openvpn-${name}-down" downScript}"}
+              "down ${pkgs.writeShellScript "openvpn-${name}-down" downScript}"}
           ${optionalString (cfg.authUserPass != null)
               "auth-user-pass ${pkgs.writeText "openvpn-credentials-${name}" ''
                 ${cfg.authUserPass.username}
@@ -57,7 +55,8 @@ let
               ''}"}
         '';
 
-    in {
+    in
+    {
       description = "OpenVPN instance ‘${name}’";
 
       wantedBy = optional cfg.autoStart "multi-user.target";
@@ -70,6 +69,16 @@ let
       serviceConfig.Type = "notify";
     };
 
+  restartService = optionalAttrs cfg.restartAfterSleep {
+    openvpn-restart = {
+      wantedBy = [ "sleep.target" ];
+      path = [ pkgs.procps ];
+      script = "pkill --signal SIGHUP --exact openvpn";
+      #SIGHUP makes openvpn process to self-exit and then it got restarted by systemd because of Restart=always
+      description = "Sends a signal to OpenVPN process to trigger a restart after return from sleep";
+    };
+  };
+
 in
 
 {
@@ -82,7 +91,7 @@ in
   options = {
 
     services.openvpn.servers = mkOption {
-      default = {};
+      default = { };
 
       example = literalExpression ''
         {
@@ -201,14 +210,21 @@ in
 
     };
 
+    services.openvpn.restartAfterSleep = mkOption {
+      default = true;
+      type = types.bool;
+      description = lib.mdDoc "Whether OpenVPN client should be restarted after sleep.";
+    };
+
   };
 
 
   ###### implementation
 
-  config = mkIf (cfg.servers != {}) {
+  config = mkIf (cfg.servers != { }) {
 
-    systemd.services = listToAttrs (mapAttrsFlatten (name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)) cfg.servers);
+    systemd.services = (listToAttrs (mapAttrsFlatten (name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)) cfg.servers))
+      // restartService;
 
     environment.systemPackages = [ openvpn ];
 
diff --git a/nixos/modules/services/networking/pdns-recursor.nix b/nixos/modules/services/networking/pdns-recursor.nix
index 2f07cefc736..f929532ba09 100644
--- a/nixos/modules/services/networking/pdns-recursor.nix
+++ b/nixos/modules/services/networking/pdns-recursor.nix
@@ -159,6 +159,8 @@ in {
 
   config = mkIf cfg.enable {
 
+    environment.etc."pdns-recursor".source = configDir;
+
     services.pdns-recursor.settings = mkDefaultAttrs {
       local-address = cfg.dns.address;
       local-port    = cfg.dns.port;
diff --git a/nixos/modules/services/networking/peroxide.nix b/nixos/modules/services/networking/peroxide.nix
new file mode 100644
index 00000000000..885ee1d96cd
--- /dev/null
+++ b/nixos/modules/services/networking/peroxide.nix
@@ -0,0 +1,131 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.peroxide;
+  settingsFormat = pkgs.formats.yaml { };
+  stateDir = "peroxide";
+in
+{
+  options.services.peroxide = {
+    enable = mkEnableOption (lib.mdDoc "peroxide");
+
+    package = mkPackageOptionMD pkgs "peroxide" {
+      default = [ "peroxide" ];
+    };
+
+    logLevel = mkOption {
+      # https://github.com/sirupsen/logrus#level-logging
+      type = types.enum [ "Panic" "Fatal" "Error" "Warning" "Info" "Debug" "Trace" ];
+      default = "Warning";
+      example = "Info";
+      description = lib.mdDoc "Only log messages of this priority or higher.";
+    };
+
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+
+        options = {
+          UserPortImap = mkOption {
+            type = types.port;
+            default = 1143;
+            description = lib.mdDoc "The port on which to listen for IMAP connections.";
+          };
+
+          UserPortSmtp = mkOption {
+            type = types.port;
+            default = 1025;
+            description = lib.mdDoc "The port on which to listen for SMTP connections.";
+          };
+
+          ServerAddress = mkOption {
+            type = types.str;
+            default = "[::0]";
+            example = "localhost";
+            description = lib.mdDoc "The address on which to listen for connections.";
+          };
+        };
+      };
+      default = { };
+      description = lib.mdDoc ''
+        Configuration for peroxide.  See
+        [config.example.yaml](https://github.com/ljanyst/peroxide/blob/master/config.example.yaml)
+        for an example configuration.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.peroxide.settings = {
+      # peroxide deletes the cache directory on startup, which requires write
+      # permission on the parent directory, so we can't use
+      # /var/cache/peroxide
+      CacheDir = "/var/cache/peroxide/cache";
+      X509Key = mkDefault "/var/lib/${stateDir}/key.pem";
+      X509Cert = mkDefault "/var/lib/${stateDir}/cert.pem";
+      CookieJar = "/var/lib/${stateDir}/cookies.json";
+      CredentialsStore = "/var/lib/${stateDir}/credentials.json";
+    };
+
+    users.users.peroxide = {
+      isSystemUser = true;
+      group = "peroxide";
+    };
+    users.groups.peroxide = { };
+
+    systemd.services.peroxide = {
+      description = "Peroxide ProtonMail bridge";
+      requires = [ "network.target" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      restartTriggers = [ config.environment.etc."peroxide.conf".source ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = "peroxide";
+        LogsDirectory = "peroxide";
+        LogsDirectoryMode = "0750";
+        # Specify just "peroxide" so that the user has write permission, because
+        # peroxide deletes and recreates the cache directory on startup.
+        CacheDirectory = [ "peroxide" "peroxide/cache" ];
+        CacheDirectoryMode = "0700";
+        StateDirectory = stateDir;
+        StateDirectoryMode = "0700";
+        ExecStart = "${cfg.package}/bin/peroxide -log-file=/var/log/peroxide/peroxide.log -log-level ${cfg.logLevel}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      };
+
+      preStart = ''
+        # Create a self-signed certificate if no certificate exists.
+        if [[ ! -e "${cfg.settings.X509Key}" && ! -e "${cfg.settings.X509Cert}" ]]; then
+            ${cfg.package}/bin/peroxide-cfg -action gen-x509 \
+              -x509-org 'N/A' \
+              -x509-cn 'nixos' \
+              -x509-cert "${cfg.settings.X509Cert}" \
+              -x509-key "${cfg.settings.X509Key}"
+        fi
+      '';
+    };
+
+    # https://github.com/ljanyst/peroxide/blob/master/peroxide.logrotate
+    services.logrotate.settings.peroxide = {
+      files = "/var/log/peroxide/peroxide.log";
+      rotate = 31;
+      frequency = "daily";
+      compress = true;
+      delaycompress = true;
+      missingok = true;
+      notifempty = true;
+      su = "peroxide peroxide";
+      postrotate = "systemctl reload peroxide";
+    };
+
+    environment.etc."peroxide.conf".source = settingsFormat.generate "peroxide.conf" cfg.settings;
+    environment.systemPackages = [ cfg.package ];
+  };
+
+  meta.maintainers = with maintainers; [ aanderse aidalgol ];
+}
diff --git a/nixos/modules/services/networking/picosnitch.nix b/nixos/modules/services/networking/picosnitch.nix
new file mode 100644
index 00000000000..c9b38c1929c
--- /dev/null
+++ b/nixos/modules/services/networking/picosnitch.nix
@@ -0,0 +1,26 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.picosnitch;
+in
+{
+  options.services.picosnitch = {
+    enable = mkEnableOption (lib.mdDoc "picosnitch daemon");
+  };
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.picosnitch ];
+    systemd.services.picosnitch = {
+      description = "picosnitch";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+        RestartSec = 5;
+        ExecStart = "${pkgs.picosnitch}/bin/picosnitch start-no-daemon";
+        PIDFile = "/run/picosnitch/picosnitch.pid";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/pleroma.md b/nixos/modules/services/networking/pleroma.md
new file mode 100644
index 00000000000..7c499e1c616
--- /dev/null
+++ b/nixos/modules/services/networking/pleroma.md
@@ -0,0 +1,180 @@
+# Pleroma {#module-services-pleroma}
+
+[Pleroma](https://pleroma.social/) is a lightweight activity pub server.
+
+## Generating the Pleroma config {#module-services-pleroma-generate-config}
+
+The `pleroma_ctl` CLI utility will prompt you some questions and it will generate an initial config file. This is an example of usage
+```ShellSession
+$ mkdir tmp-pleroma
+$ cd tmp-pleroma
+$ nix-shell -p pleroma-otp
+$ pleroma_ctl instance gen --output config.exs --output-psql setup.psql
+```
+
+The `config.exs` file can be further customized following the instructions on the [upstream documentation](https://docs-develop.pleroma.social/backend/configuration/cheatsheet/). Many refinements can be applied also after the service is running.
+
+## Initializing the database {#module-services-pleroma-initialize-db}
+
+First, the Postgresql service must be enabled in the NixOS configuration
+```
+services.postgresql = {
+  enable = true;
+  package = pkgs.postgresql_13;
+};
+```
+and activated with the usual
+```ShellSession
+$ nixos-rebuild switch
+```
+
+Then you can create and seed the database, using the `setup.psql` file that you generated in the previous section, by running
+```ShellSession
+$ sudo -u postgres psql -f setup.psql
+```
+
+## Enabling the Pleroma service locally {#module-services-pleroma-enable}
+
+In this section we will enable the Pleroma service only locally, so its configurations can be improved incrementally.
+
+This is an example of configuration, where [](#opt-services.pleroma.configs) option contains the content of the file `config.exs`, generated [in the first section](#module-services-pleroma-generate-config), but with the secrets (database password, endpoint secret key, salts, etc.) removed. Removing secrets is important, because otherwise they will be stored publicly in the Nix store.
+```
+services.pleroma = {
+  enable = true;
+  secretConfigFile = "/var/lib/pleroma/secrets.exs";
+  configs = [
+    ''
+    import Config
+
+    config :pleroma, Pleroma.Web.Endpoint,
+      url: [host: "pleroma.example.net", scheme: "https", port: 443],
+      http: [ip: {127, 0, 0, 1}, port: 4000]
+
+    config :pleroma, :instance,
+      name: "Test",
+      email: "admin@example.net",
+      notify_email: "admin@example.net",
+      limit: 5000,
+      registrations_open: true
+
+    config :pleroma, :media_proxy,
+      enabled: false,
+      redirect_on_failure: true
+
+    config :pleroma, Pleroma.Repo,
+      adapter: Ecto.Adapters.Postgres,
+      username: "pleroma",
+      database: "pleroma",
+      hostname: "localhost"
+
+    # Configure web push notifications
+    config :web_push_encryption, :vapid_details,
+      subject: "mailto:admin@example.net"
+
+    # ... TO CONTINUE ...
+    ''
+  ];
+};
+```
+
+Secrets must be moved into a file pointed by [](#opt-services.pleroma.secretConfigFile), in our case `/var/lib/pleroma/secrets.exs`. This file can be created copying the previously generated `config.exs` file and then removing all the settings, except the secrets. This is an example
+```
+# Pleroma instance passwords
+
+import Config
+
+config :pleroma, Pleroma.Web.Endpoint,
+   secret_key_base: "<the secret generated by pleroma_ctl>",
+   signing_salt: "<the secret generated by pleroma_ctl>"
+
+config :pleroma, Pleroma.Repo,
+  password: "<the secret generated by pleroma_ctl>"
+
+# Configure web push notifications
+config :web_push_encryption, :vapid_details,
+  public_key: "<the secret generated by pleroma_ctl>",
+  private_key: "<the secret generated by pleroma_ctl>"
+
+# ... TO CONTINUE ...
+```
+Note that the lines of the same configuration group are comma separated (i.e. all the lines end with a comma, except the last one), so when the lines with passwords are added or removed, commas must be adjusted accordingly.
+
+The service can be enabled with the usual
+```ShellSession
+$ nixos-rebuild switch
+```
+
+The service is accessible only from the local `127.0.0.1:4000` port. It can be tested using a port forwarding like this
+```ShellSession
+$ ssh -L 4000:localhost:4000 myuser@example.net
+```
+and then accessing <http://localhost:4000> from a web browser.
+
+## Creating the admin user {#module-services-pleroma-admin-user}
+
+After Pleroma service is running, all [Pleroma administration utilities](https://docs-develop.pleroma.social/) can be used. In particular an admin user can be created with
+```ShellSession
+$ pleroma_ctl user new <nickname> <email>  --admin --moderator --password <password>
+```
+
+## Configuring Nginx {#module-services-pleroma-nginx}
+
+In this configuration, Pleroma is listening only on the local port 4000. Nginx can be configured as a Reverse Proxy, for forwarding requests from public ports to the Pleroma service. This is an example of configuration, using
+[Let's Encrypt](https://letsencrypt.org/) for the TLS certificates
+```
+security.acme = {
+  email = "root@example.net";
+  acceptTerms = true;
+};
+
+services.nginx = {
+  enable = true;
+  addSSL = true;
+
+  recommendedTlsSettings = true;
+  recommendedOptimisation = true;
+  recommendedGzipSettings = true;
+
+  recommendedProxySettings = false;
+  # NOTE: if enabled, the NixOS proxy optimizations will override the Pleroma
+  # specific settings, and they will enter in conflict.
+
+  virtualHosts = {
+    "pleroma.example.net" = {
+      http2 = true;
+      enableACME = true;
+      forceSSL = true;
+
+      locations."/" = {
+        proxyPass = "http://127.0.0.1:4000";
+
+        extraConfig = ''
+          etag on;
+          gzip on;
+
+          add_header 'Access-Control-Allow-Origin' '*' always;
+          add_header 'Access-Control-Allow-Methods' 'POST, PUT, DELETE, GET, PATCH, OPTIONS' always;
+          add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Idempotency-Key' always;
+          add_header 'Access-Control-Expose-Headers' 'Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id' always;
+          if ($request_method = OPTIONS) {
+            return 204;
+          }
+          add_header X-XSS-Protection "1; mode=block";
+          add_header X-Permitted-Cross-Domain-Policies none;
+          add_header X-Frame-Options DENY;
+          add_header X-Content-Type-Options nosniff;
+          add_header Referrer-Policy same-origin;
+          add_header X-Download-Options noopen;
+          proxy_http_version 1.1;
+          proxy_set_header Upgrade $http_upgrade;
+          proxy_set_header Connection "upgrade";
+          proxy_set_header Host $host;
+
+          client_max_body_size 16m;
+          # NOTE: increase if users need to upload very big files
+        '';
+      };
+    };
+  };
+};
+```
diff --git a/nixos/modules/services/networking/pleroma.nix b/nixos/modules/services/networking/pleroma.nix
index f317510258b..e9db7f3eab8 100644
--- a/nixos/modules/services/networking/pleroma.nix
+++ b/nixos/modules/services/networking/pleroma.nix
@@ -147,5 +147,5 @@ in {
 
   };
   meta.maintainers = with lib.maintainers; [ ninjatrappeur ];
-  meta.doc = ./pleroma.xml;
+  meta.doc = ./pleroma.md;
 }
diff --git a/nixos/modules/services/networking/pleroma.xml b/nixos/modules/services/networking/pleroma.xml
deleted file mode 100644
index ad0a481af28..00000000000
--- a/nixos/modules/services/networking/pleroma.xml
+++ /dev/null
@@ -1,188 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-pleroma">
- <title>Pleroma</title>
- <para>
-  <link xlink:href="https://pleroma.social/">Pleroma</link> is a lightweight activity pub server.</para>
- <section xml:id="module-services-pleroma-generate-config">
-  <title>Generating the Pleroma config</title>
-  <para>The <literal>pleroma_ctl</literal> CLI utility will prompt you some questions and it will generate an initial config file. This is an example of usage
-<programlisting>
-<prompt>$ </prompt>mkdir tmp-pleroma
-<prompt>$ </prompt>cd tmp-pleroma
-<prompt>$ </prompt>nix-shell -p pleroma-otp
-<prompt>$ </prompt>pleroma_ctl instance gen --output config.exs --output-psql setup.psql
-</programlisting>
-  </para>
-  <para>The <literal>config.exs</literal> file can be further customized following the instructions on the <link xlink:href="https://docs-develop.pleroma.social/backend/configuration/cheatsheet/">upstream documentation</link>. Many refinements can be applied also after the service is running.</para>
- </section>
- <section xml:id="module-services-pleroma-initialize-db">
-  <title>Initializing the database</title>
-  <para>First, the Postgresql service must be enabled in the NixOS configuration
-<programlisting>
-services.postgresql = {
-  enable = true;
-  package = pkgs.postgresql_13;
-};
-</programlisting>
-and activated with the usual
-<programlisting>
-<prompt>$ </prompt>nixos-rebuild switch
-</programlisting>
-  </para>
-  <para>Then you can create and seed the database, using the <literal>setup.psql</literal> file that you generated in the previous section, by running
-<programlisting>
-<prompt>$ </prompt>sudo -u postgres psql -f setup.psql
-</programlisting>
-  </para>
- </section>
- <section xml:id="module-services-pleroma-enable">
-  <title>Enabling the Pleroma service locally</title>
-  <para>In this section we will enable the Pleroma service only locally, so its configurations can be improved incrementally.</para>
-  <para>This is an example of configuration, where <link linkend="opt-services.pleroma.configs">services.pleroma.configs</link> option contains the content of the file <literal>config.exs</literal>, generated <link linkend="module-services-pleroma-generate-config">in the first section</link>, but with the secrets (database password, endpoint secret key, salts, etc.) removed. Removing secrets is important, because otherwise they will be stored publicly in the Nix store.
-<programlisting>
-services.pleroma = {
-  enable = true;
-  secretConfigFile = "/var/lib/pleroma/secrets.exs";
-  configs = [
-    ''
-    import Config
-
-    config :pleroma, Pleroma.Web.Endpoint,
-      url: [host: "pleroma.example.net", scheme: "https", port: 443],
-      http: [ip: {127, 0, 0, 1}, port: 4000]
-
-    config :pleroma, :instance,
-      name: "Test",
-      email: "admin@example.net",
-      notify_email: "admin@example.net",
-      limit: 5000,
-      registrations_open: true
-
-    config :pleroma, :media_proxy,
-      enabled: false,
-      redirect_on_failure: true
-
-    config :pleroma, Pleroma.Repo,
-      adapter: Ecto.Adapters.Postgres,
-      username: "pleroma",
-      database: "pleroma",
-      hostname: "localhost"
-
-    # Configure web push notifications
-    config :web_push_encryption, :vapid_details,
-      subject: "mailto:admin@example.net"
-
-    # ... TO CONTINUE ...
-    ''
-  ];
-};
-</programlisting>
-  </para>
-  <para>Secrets must be moved into a file pointed by <link linkend="opt-services.pleroma.secretConfigFile">services.pleroma.secretConfigFile</link>, in our case <literal>/var/lib/pleroma/secrets.exs</literal>. This file can be created copying the previously generated <literal>config.exs</literal> file and then removing all the settings, except the secrets. This is an example
-<programlisting>
-# Pleroma instance passwords
-
-import Config
-
-config :pleroma, Pleroma.Web.Endpoint,
-   secret_key_base: "&lt;the secret generated by pleroma_ctl&gt;",
-   signing_salt: "&lt;the secret generated by pleroma_ctl&gt;"
-
-config :pleroma, Pleroma.Repo,
-  password: "&lt;the secret generated by pleroma_ctl&gt;"
-
-# Configure web push notifications
-config :web_push_encryption, :vapid_details,
-  public_key: "&lt;the secret generated by pleroma_ctl&gt;",
-  private_key: "&lt;the secret generated by pleroma_ctl&gt;"
-
-# ... TO CONTINUE ...
-</programlisting>
-  Note that the lines of the same configuration group are comma separated (i.e. all the lines end with a comma, except the last one), so when the lines with passwords are added or removed, commas must be adjusted accordingly.</para>
-
-  <para>The service can be enabled with the usual
-<programlisting>
-<prompt>$ </prompt>nixos-rebuild switch
-</programlisting>
-  </para>
-  <para>The service is accessible only from the local <literal>127.0.0.1:4000</literal> port. It can be tested using a port forwarding like this
-<programlisting>
-<prompt>$ </prompt>ssh -L 4000:localhost:4000 myuser@example.net
-</programlisting>
-and then accessing <link xlink:href="http://localhost:4000">http://localhost:4000</link> from a web browser.</para>
- </section>
- <section xml:id="module-services-pleroma-admin-user">
-  <title>Creating the admin user</title>
-  <para>After Pleroma service is running, all <link xlink:href="https://docs-develop.pleroma.social/">Pleroma administration utilities</link> can be used. In particular an admin user can be created with
-<programlisting>
-<prompt>$ </prompt>pleroma_ctl user new &lt;nickname&gt; &lt;email&gt;  --admin --moderator --password &lt;password&gt;
-</programlisting>
-  </para>
- </section>
- <section xml:id="module-services-pleroma-nginx">
-  <title>Configuring Nginx</title>
-  <para>In this configuration, Pleroma is listening only on the local port 4000. Nginx can be configured as a Reverse Proxy, for forwarding requests from public ports to the Pleroma service. This is an example of configuration, using
-<link xlink:href="https://letsencrypt.org/">Let's Encrypt</link> for the TLS certificates
-<programlisting>
-security.acme = {
-  email = "root@example.net";
-  acceptTerms = true;
-};
-
-services.nginx = {
-  enable = true;
-  addSSL = true;
-
-  recommendedTlsSettings = true;
-  recommendedOptimisation = true;
-  recommendedGzipSettings = true;
-
-  recommendedProxySettings = false;
-  # NOTE: if enabled, the NixOS proxy optimizations will override the Pleroma
-  # specific settings, and they will enter in conflict.
-
-  virtualHosts = {
-    "pleroma.example.net" = {
-      http2 = true;
-      enableACME = true;
-      forceSSL = true;
-
-      locations."/" = {
-        proxyPass = "http://127.0.0.1:4000";
-
-        extraConfig = ''
-          etag on;
-          gzip on;
-
-          add_header 'Access-Control-Allow-Origin' '*' always;
-          add_header 'Access-Control-Allow-Methods' 'POST, PUT, DELETE, GET, PATCH, OPTIONS' always;
-          add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Idempotency-Key' always;
-          add_header 'Access-Control-Expose-Headers' 'Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id' always;
-          if ($request_method = OPTIONS) {
-            return 204;
-          }
-          add_header X-XSS-Protection "1; mode=block";
-          add_header X-Permitted-Cross-Domain-Policies none;
-          add_header X-Frame-Options DENY;
-          add_header X-Content-Type-Options nosniff;
-          add_header Referrer-Policy same-origin;
-          add_header X-Download-Options noopen;
-          proxy_http_version 1.1;
-          proxy_set_header Upgrade $http_upgrade;
-          proxy_set_header Connection "upgrade";
-          proxy_set_header Host $host;
-
-          client_max_body_size 16m;
-          # NOTE: increase if users need to upload very big files
-        '';
-      };
-    };
-  };
-};
-</programlisting>
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/networking/powerdns.nix b/nixos/modules/services/networking/powerdns.nix
index 850a128cf1a..03bf93301d8 100644
--- a/nixos/modules/services/networking/powerdns.nix
+++ b/nixos/modules/services/networking/powerdns.nix
@@ -38,6 +38,8 @@ in {
 
   config = mkIf cfg.enable {
 
+    environment.etc.pdns.source = finalConfigDir;
+
     systemd.packages = [ pkgs.pdns ];
 
     systemd.services.pdns = {
diff --git a/nixos/modules/services/networking/prosody.md b/nixos/modules/services/networking/prosody.md
new file mode 100644
index 00000000000..2da2c242a98
--- /dev/null
+++ b/nixos/modules/services/networking/prosody.md
@@ -0,0 +1,72 @@
+# Prosody {#module-services-prosody}
+
+[Prosody](https://prosody.im/) is an open-source, modern XMPP server.
+
+## Basic usage {#module-services-prosody-basic-usage}
+
+A common struggle for most XMPP newcomers is to find the right set
+of XMPP Extensions (XEPs) to setup. Forget to activate a few of
+those and your XMPP experience might turn into a nightmare!
+
+The XMPP community tackles this problem by creating a meta-XEP
+listing a decent set of XEPs you should implement. This meta-XEP
+is issued every year, the 2020 edition being
+[XEP-0423](https://xmpp.org/extensions/xep-0423.html).
+
+The NixOS Prosody module will implement most of these recommendend XEPs out of
+the box. That being said, two components still require some
+manual configuration: the
+[Multi User Chat (MUC)](https://xmpp.org/extensions/xep-0045.html)
+and the [HTTP File Upload](https://xmpp.org/extensions/xep-0363.html) ones.
+You'll need to create a DNS subdomain for each of those. The current convention is to name your
+MUC endpoint `conference.example.org` and your HTTP upload domain `upload.example.org`.
+
+A good configuration to start with, including a
+[Multi User Chat (MUC)](https://xmpp.org/extensions/xep-0045.html)
+endpoint as well as a [HTTP File Upload](https://xmpp.org/extensions/xep-0363.html)
+endpoint will look like this:
+```
+services.prosody = {
+  enable = true;
+  admins = [ "root@example.org" ];
+  ssl.cert = "/var/lib/acme/example.org/fullchain.pem";
+  ssl.key = "/var/lib/acme/example.org/key.pem";
+  virtualHosts."example.org" = {
+      enabled = true;
+      domain = "example.org";
+      ssl.cert = "/var/lib/acme/example.org/fullchain.pem";
+      ssl.key = "/var/lib/acme/example.org/key.pem";
+  };
+  muc = [ {
+      domain = "conference.example.org";
+  } ];
+  uploadHttp = {
+      domain = "upload.example.org";
+  };
+};
+```
+
+## Let's Encrypt Configuration {#module-services-prosody-letsencrypt}
+
+As you can see in the code snippet from the
+[previous section](#module-services-prosody-basic-usage),
+you'll need a single TLS certificate covering your main endpoint,
+the MUC one as well as the HTTP Upload one. We can generate such a
+certificate by leveraging the ACME
+[extraDomainNames](#opt-security.acme.certs._name_.extraDomainNames) module option.
+
+Provided the setup detailed in the previous section, you'll need the following acme configuration to generate
+a TLS certificate for the three endponits:
+```
+security.acme = {
+  email = "root@example.org";
+  acceptTerms = true;
+  certs = {
+    "example.org" = {
+      webroot = "/var/www/example.org";
+      email = "root@example.org";
+      extraDomainNames = [ "conference.example.org" "upload.example.org" ];
+    };
+  };
+};
+```
diff --git a/nixos/modules/services/networking/prosody.nix b/nixos/modules/services/networking/prosody.nix
index 342638f93ba..0066c77438f 100644
--- a/nixos/modules/services/networking/prosody.nix
+++ b/nixos/modules/services/networking/prosody.nix
@@ -757,9 +757,8 @@ in
 
     environment.etc."prosody/prosody.cfg.lua".text =
       let
-        httpDiscoItems = if (cfg.uploadHttp != null)
-            then [{ url = cfg.uploadHttp.domain; description = "HTTP upload endpoint";}]
-            else [];
+        httpDiscoItems = optionals (cfg.uploadHttp != null)
+            [{ url = cfg.uploadHttp.domain; description = "HTTP upload endpoint";}];
         mucDiscoItems = builtins.foldl'
             (acc: muc: [{ url = muc.domain; description = "${muc.domain} MUC endpoint";}] ++ acc)
             []
@@ -904,5 +903,6 @@ in
     };
 
   };
-  meta.doc = ./prosody.xml;
+
+  meta.doc = ./prosody.md;
 }
diff --git a/nixos/modules/services/networking/prosody.xml b/nixos/modules/services/networking/prosody.xml
deleted file mode 100644
index 6358d744ff7..00000000000
--- a/nixos/modules/services/networking/prosody.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-prosody">
- <title>Prosody</title>
- <para>
-  <link xlink:href="https://prosody.im/">Prosody</link> is an open-source, modern XMPP server.
- </para>
- <section xml:id="module-services-prosody-basic-usage">
-  <title>Basic usage</title>
-
-  <para>
-    A common struggle for most XMPP newcomers is to find the right set
-    of XMPP Extensions (XEPs) to setup. Forget to activate a few of
-    those and your XMPP experience might turn into a nightmare!
-  </para>
-
-  <para>
-    The XMPP community tackles this problem by creating a meta-XEP
-    listing a decent set of XEPs you should implement. This meta-XEP
-    is issued every year, the 2020 edition being
-    <link xlink:href="https://xmpp.org/extensions/xep-0423.html">XEP-0423</link>.
-  </para>
-  <para>
-    The NixOS Prosody module will implement most of these recommendend XEPs out of
-    the box. That being said, two components still require some
-    manual configuration: the
-    <link xlink:href="https://xmpp.org/extensions/xep-0045.html">Multi User Chat (MUC)</link>
-    and the <link xlink:href="https://xmpp.org/extensions/xep-0363.html">HTTP File Upload</link> ones.
-    You'll need to create a DNS subdomain for each of those. The current convention is to name your
-    MUC endpoint <literal>conference.example.org</literal> and your HTTP upload domain <literal>upload.example.org</literal>.
-  </para>
-  <para>
-    A good configuration to start with, including a
-    <link xlink:href="https://xmpp.org/extensions/xep-0045.html">Multi User Chat (MUC)</link>
-    endpoint as well as a <link xlink:href="https://xmpp.org/extensions/xep-0363.html">HTTP File Upload</link>
-    endpoint will look like this:
-    <programlisting>
-services.prosody = {
-  <link linkend="opt-services.prosody.enable">enable</link> = true;
-  <link linkend="opt-services.prosody.admins">admins</link> = [ "root@example.org" ];
-  <link linkend="opt-services.prosody.ssl.cert">ssl.cert</link> = "/var/lib/acme/example.org/fullchain.pem";
-  <link linkend="opt-services.prosody.ssl.key">ssl.key</link> = "/var/lib/acme/example.org/key.pem";
-  <link linkend="opt-services.prosody.virtualHosts">virtualHosts</link>."example.org" = {
-      <link linkend="opt-services.prosody.virtualHosts._name_.enabled">enabled</link> = true;
-      <link linkend="opt-services.prosody.virtualHosts._name_.domain">domain</link> = "example.org";
-      <link linkend="opt-services.prosody.virtualHosts._name_.ssl.cert">ssl.cert</link> = "/var/lib/acme/example.org/fullchain.pem";
-      <link linkend="opt-services.prosody.virtualHosts._name_.ssl.key">ssl.key</link> = "/var/lib/acme/example.org/key.pem";
-  };
-  <link linkend="opt-services.prosody.muc">muc</link> = [ {
-      <link linkend="opt-services.prosody.muc">domain</link> = "conference.example.org";
-  } ];
-  <link linkend="opt-services.prosody.uploadHttp">uploadHttp</link> = {
-      <link linkend="opt-services.prosody.uploadHttp.domain">domain</link> = "upload.example.org";
-  };
-};</programlisting>
-  </para>
- </section>
- <section xml:id="module-services-prosody-letsencrypt">
-  <title>Let's Encrypt Configuration</title>
- <para>
-   As you can see in the code snippet from the
-   <link linkend="module-services-prosody-basic-usage">previous section</link>,
-   you'll need a single TLS certificate covering your main endpoint,
-   the MUC one as well as the HTTP Upload one. We can generate such a
-   certificate by leveraging the ACME
-   <link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> module option.
- </para>
- <para>
-   Provided the setup detailed in the previous section, you'll need the following acme configuration to generate
-   a TLS certificate for the three endponits:
-    <programlisting>
-security.acme = {
-  <link linkend="opt-security.acme.defaults.email">email</link> = "root@example.org";
-  <link linkend="opt-security.acme.acceptTerms">acceptTerms</link> = true;
-  <link linkend="opt-security.acme.certs">certs</link> = {
-    "example.org" = {
-      <link linkend="opt-security.acme.certs._name_.webroot">webroot</link> = "/var/www/example.org";
-      <link linkend="opt-security.acme.certs._name_.email">email</link> = "root@example.org";
-      <link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> = [ "conference.example.org" "upload.example.org" ];
-    };
-  };
-};</programlisting>
- </para>
-</section>
-</chapter>
diff --git a/nixos/modules/services/networking/radicale.nix b/nixos/modules/services/networking/radicale.nix
index 9ec507fe2ab..00dbd6bbe38 100644
--- a/nixos/modules/services/networking/radicale.nix
+++ b/nixos/modules/services/networking/radicale.nix
@@ -9,7 +9,7 @@ let
     listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
   };
 
-  pkg = if isNull cfg.package then
+  pkg = if cfg.package == null then
     pkgs.radicale
   else
     cfg.package;
@@ -117,13 +117,13 @@ in {
       }
     ];
 
-    warnings = optional (isNull cfg.package && versionOlder config.system.stateVersion "17.09") ''
+    warnings = optional (cfg.package == null && versionOlder config.system.stateVersion "17.09") ''
       The configuration and storage formats of your existing Radicale
       installation might be incompatible with the newest version.
       For upgrade instructions see
       https://radicale.org/2.1.html#documentation/migration-from-1xx-to-2xx.
       Set services.radicale.package to suppress this warning.
-    '' ++ optional (isNull cfg.package && versionOlder config.system.stateVersion "20.09") ''
+    '' ++ optional (cfg.package == null && versionOlder config.system.stateVersion "20.09") ''
       The configuration format of your existing Radicale installation might be
       incompatible with the newest version.  For upgrade instructions see
       https://github.com/Kozea/Radicale/blob/3.0.6/NEWS.md#upgrade-checklist.
@@ -200,5 +200,5 @@ in {
     };
   };
 
-  meta.maintainers = with lib.maintainers; [ aneeshusa infinisil dotlambda ];
+  meta.maintainers = with lib.maintainers; [ infinisil dotlambda ];
 }
diff --git a/nixos/modules/services/networking/redsocks.nix b/nixos/modules/services/networking/redsocks.nix
index 45feb1313c9..30d6a0a6336 100644
--- a/nixos/modules/services/networking/redsocks.nix
+++ b/nixos/modules/services/networking/redsocks.nix
@@ -37,7 +37,7 @@ in
               - stderr
               - file:/path/to/file
               - syslog:FACILITY where FACILITY is any of "daemon", "local0",
-              etc.
+                etc.
           '';
       };
 
@@ -125,6 +125,7 @@ in
               lib.mdDoc ''
                 Way to disclose client IP to the proxy.
                   - "false": do not disclose
+
                 http-connect supports the following ways:
                   - "X-Forwarded-For": add header "X-Forwarded-For: IP"
                   - "Forwarded_ip": add header "Forwarded: for=IP" (see RFC7239)
diff --git a/nixos/modules/services/networking/rpcbind.nix b/nixos/modules/services/networking/rpcbind.nix
index 60e78dfec51..63c4859fbd0 100644
--- a/nixos/modules/services/networking/rpcbind.nix
+++ b/nixos/modules/services/networking/rpcbind.nix
@@ -14,7 +14,7 @@ with lib;
         type = types.bool;
         default = false;
         description = lib.mdDoc ''
-          Whether to enable `rpcbind', an ONC RPC directory service
+          Whether to enable `rpcbind`, an ONC RPC directory service
           notably used by NFS and NIS, and which can be queried
           using the rpcinfo(1) command. `rpcbind` is a replacement for
           `portmap`.
diff --git a/nixos/modules/services/networking/searx.nix b/nixos/modules/services/networking/searx.nix
index 6c57ddbde2d..40648c72481 100644
--- a/nixos/modules/services/networking/searx.nix
+++ b/nixos/modules/services/networking/searx.nix
@@ -10,6 +10,8 @@ let
   settingsFile = pkgs.writeText "settings.yml"
     (builtins.toJSON cfg.settings);
 
+  limiterSettingsFile = (pkgs.formats.toml { }).generate "limiter.toml" cfg.limiterSettings;
+
   generateConfig = ''
     cd ${runDir}
 
@@ -65,6 +67,15 @@ in
         '';
       };
 
+      redisCreateLocally = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Configure a local Redis server for SearXNG. This is required if you
+          want to enable the rate limiter and bot protection of SearXNG.
+        '';
+      };
+
       settings = mkOption {
         type = types.attrsOf settingType;
         default = { };
@@ -111,6 +122,31 @@ in
         '';
       };
 
+      limiterSettings = mkOption {
+        type = types.attrsOf settingType;
+        default = { };
+        example = literalExpression ''
+          {
+            real_ip = {
+              x_for = 1;
+              ipv4_prefix = 32;
+              ipv6_prefix = 56;
+            }
+            botdetection.ip_lists.block_ip = [
+              # "93.184.216.34" # example.org
+            ];
+          }
+        '';
+        description = lib.mdDoc ''
+          Limiter settings for SearXNG.
+
+          ::: {.note}
+          For available settings, see the SearXNG
+          [schema file](https://github.com/searxng/searxng/blob/master/searx/botdetection/limiter.toml).
+          :::
+        '';
+      };
+
       package = mkOption {
         type = types.package;
         default = pkgs.searx;
@@ -158,6 +194,17 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = (cfg.limiterSettings != { }) -> cfg.package.pname == "searxng";
+        message = "services.searx.limiterSettings requires services.searx.package to be searxng.";
+      }
+      {
+        assertion = cfg.redisCreateLocally -> cfg.package.pname == "searxng";
+        message = "services.searx.redisCreateLocally requires services.searx.package to be searxng.";
+      }
+    ];
+
     environment.systemPackages = [ cfg.package ];
 
     users.users.searx =
@@ -206,6 +253,7 @@ in
     services.searx.settings = {
       # merge NixOS settings with defaults settings.yml
       use_default_settings = mkDefault true;
+      redis.url = lib.mkIf cfg.redisCreateLocally "unix://${config.services.redis.servers.searx.unixSocket}";
     };
 
     services.uwsgi = mkIf (cfg.runInUwsgi) {
@@ -231,7 +279,16 @@ in
       } // cfg.uwsgiConfig;
     };
 
+    services.redis.servers.searx = lib.mkIf cfg.redisCreateLocally {
+      enable = true;
+      user = "searx";
+      port = 0;
+    };
+
+    environment.etc."searxng/limiter.toml" = lib.mkIf (cfg.limiterSettings != { }) {
+      source = limiterSettingsFile;
+    };
   };
 
-  meta.maintainers = with maintainers; [ rnhmjoj ];
+  meta.maintainers = with maintainers; [ rnhmjoj _999eagle ];
 }
diff --git a/nixos/modules/services/networking/shellhub-agent.nix b/nixos/modules/services/networking/shellhub-agent.nix
index ad33c50f9d6..7cce23cb9c4 100644
--- a/nixos/modules/services/networking/shellhub-agent.nix
+++ b/nixos/modules/services/networking/shellhub-agent.nix
@@ -14,7 +14,7 @@ in
 
       enable = mkEnableOption (lib.mdDoc "ShellHub Agent daemon");
 
-      package = mkPackageOption pkgs "shellhub-agent" { };
+      package = mkPackageOptionMD pkgs "shellhub-agent" { };
 
       preferredHostname = mkOption {
         type = types.str;
diff --git a/nixos/modules/services/networking/sing-box.nix b/nixos/modules/services/networking/sing-box.nix
new file mode 100644
index 00000000000..a884bcd271e
--- /dev/null
+++ b/nixos/modules/services/networking/sing-box.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, utils, ... }:
+let
+  cfg = config.services.sing-box;
+  settingsFormat = pkgs.formats.json { };
+in
+{
+
+  meta = {
+    maintainers = with lib.maintainers; [ nickcao ];
+  };
+
+  options = {
+    services.sing-box = {
+      enable = lib.mkEnableOption (lib.mdDoc "sing-box universal proxy platform");
+
+      package = lib.mkPackageOptionMD pkgs "sing-box" { };
+
+      settings = lib.mkOption {
+        type = lib.types.submodule {
+          freeformType = settingsFormat.type;
+          options = {
+            route = {
+              geoip.path = lib.mkOption {
+                type = lib.types.path;
+                default = "${pkgs.sing-geoip}/share/sing-box/geoip.db";
+                defaultText = lib.literalExpression "\${pkgs.sing-geoip}/share/sing-box/geoip.db";
+                description = lib.mdDoc ''
+                  The path to the sing-geoip database.
+                '';
+              };
+              geosite.path = lib.mkOption {
+                type = lib.types.path;
+                default = "${pkgs.sing-geosite}/share/sing-box/geosite.db";
+                defaultText = lib.literalExpression "\${pkgs.sing-geosite}/share/sing-box/geosite.db";
+                description = lib.mdDoc ''
+                  The path to the sing-geosite database.
+                '';
+              };
+            };
+          };
+        };
+        default = { };
+        description = lib.mdDoc ''
+          The sing-box configuration, see https://sing-box.sagernet.org/configuration/ for documentation.
+
+          Options containing secret data should be set to an attribute set
+          containing the attribute `_secret` - a string pointing to a file
+          containing the value the option should be set to.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.packages = [ cfg.package ];
+
+    systemd.services.sing-box = {
+      preStart = ''
+        umask 0077
+        mkdir -p /etc/sing-box
+        ${utils.genJqSecretsReplacementSnippet cfg.settings "/etc/sing-box/config.json"}
+      '';
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+
+}
diff --git a/nixos/modules/services/networking/sitespeed-io.nix b/nixos/modules/services/networking/sitespeed-io.nix
new file mode 100644
index 00000000000..f7eab0bb19d
--- /dev/null
+++ b/nixos/modules/services/networking/sitespeed-io.nix
@@ -0,0 +1,122 @@
+{ lib, config, pkgs, ... }:
+let
+  cfg = config.services.sitespeed-io;
+  format = pkgs.formats.json { };
+in
+{
+  options.services.sitespeed-io = {
+    enable = lib.mkEnableOption (lib.mdDoc "Sitespeed.io");
+
+    user = lib.mkOption {
+      type = lib.types.str;
+      default = "sitespeed-io";
+      description = lib.mdDoc "User account under which sitespeed-io runs.";
+    };
+
+    package = lib.mkOption {
+      type = lib.types.package;
+      default = pkgs.sitespeed-io;
+      defaultText = "pkgs.sitespeed-io";
+      description = lib.mdDoc "Sitespeed.io package to use.";
+    };
+
+    dataDir = lib.mkOption {
+      default = "/var/lib/sitespeed-io";
+      type = lib.types.str;
+      description = lib.mdDoc "The base sitespeed-io data directory.";
+    };
+
+    period = lib.mkOption {
+      type = lib.types.str;
+      default = "hourly";
+      description = lib.mdDoc ''
+        Systemd calendar expression when to run. See {manpage}`systemd.time(7)`.
+      '';
+    };
+
+    runs = lib.mkOption {
+      default = [ ];
+      description = lib.mdDoc ''
+        A list of run configurations. The service will call sitespeed-io once
+        for every run listed here. This lets you examine different websites
+        with different sitespeed-io settings.
+      '';
+      type = lib.types.listOf (lib.types.submodule {
+        options = {
+          urls = lib.mkOption {
+            type = with lib.types; listOf str;
+            default = [];
+            description = lib.mdDoc ''
+              URLs the service should monitor.
+            '';
+          };
+
+          settings = lib.mkOption {
+            type = lib.types.submodule {
+              freeformType = format.type;
+              options = { };
+            };
+            default = { };
+            description = lib.mdDoc ''
+              Configuration for sitespeed-io, see
+              <https://www.sitespeed.io/documentation/sitespeed.io/configuration/>
+              for available options. The value here will be directly transformed to
+              JSON and passed as `--config` to the program.
+            '';
+          };
+
+          extraArgs = lib.mkOption {
+            type = with lib.types; listOf str;
+            default = [];
+            description = lib.mdDoc ''
+              Extra command line arguments to pass to the program.
+            '';
+          };
+        };
+      });
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+    {
+      assertion = cfg.runs != [];
+      message = "At least one run must be configured.";
+    }
+    {
+      assertion = lib.all (run: run.urls != []) cfg.runs;
+      message = "All runs must have at least one url configured.";
+    }
+  ];
+
+    systemd.services.sitespeed-io = {
+      description = "Check website status";
+      startAt = cfg.period;
+      serviceConfig = {
+        WorkingDirectory = cfg.dataDir;
+        User = cfg.user;
+      };
+      preStart = "chmod u+w -R ${cfg.dataDir}"; # Make sure things are writable
+      script = (lib.concatMapStrings (run: ''
+        ${lib.getExe cfg.package} \
+          --config ${format.generate "sitespeed.json" run.settings} \
+          ${lib.escapeShellArgs run.extraArgs} \
+          ${builtins.toFile "urls.txt" (lib.concatLines run.urls)} &
+      '') cfg.runs) +
+      ''
+        wait
+      '';
+    };
+
+    users = {
+      extraUsers.${cfg.user} = {
+        isSystemUser = true;
+        group = cfg.user;
+        home = cfg.dataDir;
+        createHome = true;
+        homeMode = "755";
+      };
+      extraGroups.${cfg.user} = { };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/smokeping.nix b/nixos/modules/services/networking/smokeping.nix
index 2e67f8b77c0..c7aec7d9489 100644
--- a/nixos/modules/services/networking/smokeping.nix
+++ b/nixos/modules/services/networking/smokeping.nix
@@ -8,41 +8,41 @@ let
   smokepingPidDir = "/run";
   configFile =
     if cfg.config == null
-      then
-        ''
-          *** General ***
-          cgiurl   = ${cfg.cgiUrl}
-          contact = ${cfg.ownerEmail}
-          datadir  = ${smokepingHome}/data
-          imgcache = ${smokepingHome}/cache
-          imgurl   = ${cfg.imgUrl}
-          linkstyle = ${cfg.linkStyle}
-          ${lib.optionalString (cfg.mailHost != "") "mailhost = ${cfg.mailHost}"}
-          owner = ${cfg.owner}
-          pagedir = ${smokepingHome}/cache
-          piddir  = ${smokepingPidDir}
-          ${lib.optionalString (cfg.sendmail != null) "sendmail = ${cfg.sendmail}"}
-          smokemail = ${cfg.smokeMailTemplate}
-          *** Presentation ***
-          template = ${cfg.presentationTemplate}
-          ${cfg.presentationConfig}
-          *** Alerts ***
-          ${cfg.alertConfig}
-          *** Database ***
-          ${cfg.databaseConfig}
-          *** Probes ***
-          ${cfg.probeConfig}
-          *** Targets ***
-          ${cfg.targetConfig}
-          ${cfg.extraConfig}
-        ''
-      else
-        cfg.config;
+    then
+      ''
+        *** General ***
+        cgiurl   = ${cfg.cgiUrl}
+        contact = ${cfg.ownerEmail}
+        datadir  = ${smokepingHome}/data
+        imgcache = ${smokepingHome}/cache
+        imgurl   = ${cfg.imgUrl}
+        linkstyle = ${cfg.linkStyle}
+        ${lib.optionalString (cfg.mailHost != "") "mailhost = ${cfg.mailHost}"}
+        owner = ${cfg.owner}
+        pagedir = ${smokepingHome}/cache
+        piddir  = ${smokepingPidDir}
+        ${lib.optionalString (cfg.sendmail != null) "sendmail = ${cfg.sendmail}"}
+        smokemail = ${cfg.smokeMailTemplate}
+        *** Presentation ***
+        template = ${cfg.presentationTemplate}
+        ${cfg.presentationConfig}
+        *** Alerts ***
+        ${cfg.alertConfig}
+        *** Database ***
+        ${cfg.databaseConfig}
+        *** Probes ***
+        ${cfg.probeConfig}
+        *** Targets ***
+        ${cfg.targetConfig}
+        ${cfg.extraConfig}
+      ''
+    else
+      cfg.config;
 
   configPath = pkgs.writeText "smokeping.conf" configFile;
   cgiHome = pkgs.writeScript "smokeping.fcgi" ''
     #!${pkgs.bash}/bin/bash
-    ${cfg.package}/bin/smokeping_cgi ${configPath}
+    ${cfg.package}/bin/smokeping_cgi /etc/smokeping.conf
   '';
 in
 
@@ -141,7 +141,7 @@ in
         '';
       };
       linkStyle = mkOption {
-        type = types.enum ["original" "absolute" "relative"];
+        type = types.enum [ "original" "absolute" "relative" ];
         default = "relative";
         example = "absolute";
         description = lib.mdDoc "DNS name for the urls generated in the cgi.";
@@ -301,12 +301,14 @@ in
     ];
     security.wrappers = {
       fping =
-        { setuid = true;
+        {
+          setuid = true;
           owner = "root";
           group = "root";
           source = "${pkgs.fping}/bin/fping";
         };
     };
+    environment.etc."smokeping.conf".source = configPath;
     environment.systemPackages = [ pkgs.fping ];
     users.users.${cfg.user} = {
       isNormalUser = false;
@@ -327,27 +329,27 @@ in
       # Thus, we need to make `smokepingHome` (which is given to `thttpd -d` below) `755`.
       homeMode = "755";
     };
-    users.groups.${cfg.user} = {};
+    users.groups.${cfg.user} = { };
     systemd.services.smokeping = {
-      requiredBy = [ "multi-user.target"];
+      reloadTriggers = [ configPath ];
+      requiredBy = [ "multi-user.target" ];
       serviceConfig = {
         User = cfg.user;
         Restart = "on-failure";
-        ExecStart = "${cfg.package}/bin/smokeping --config=${configPath} --nodaemon";
+        ExecStart = "${cfg.package}/bin/smokeping --config=/etc/smokeping.conf --nodaemon";
       };
       preStart = ''
         mkdir -m 0755 -p ${smokepingHome}/cache ${smokepingHome}/data
-        rm -f ${smokepingHome}/cropper
-        ln -s ${cfg.package}/htdocs/cropper ${smokepingHome}/cropper
-        rm -f ${smokepingHome}/smokeping.fcgi
-        ln -s ${cgiHome} ${smokepingHome}/smokeping.fcgi
+        ln -snf ${cfg.package}/htdocs/css ${smokepingHome}/css
+        ln -snf ${cfg.package}/htdocs/js ${smokepingHome}/js
+        ln -snf ${cgiHome} ${smokepingHome}/smokeping.fcgi
         ${cfg.package}/bin/smokeping --check --config=${configPath}
         ${cfg.package}/bin/smokeping --static --config=${configPath}
       '';
     };
     systemd.services.thttpd = mkIf cfg.webService {
-      requiredBy = [ "multi-user.target"];
-      requires = [ "smokeping.service"];
+      requiredBy = [ "multi-user.target" ];
+      requires = [ "smokeping.service" ];
       path = with pkgs; [ bash rrdtool smokeping thttpd ];
       serviceConfig = {
         Restart = "always";
diff --git a/nixos/modules/services/networking/soju.nix b/nixos/modules/services/networking/soju.nix
index d4c4ca47bc8..7f0ac3e3b8e 100644
--- a/nixos/modules/services/networking/soju.nix
+++ b/nixos/modules/services/networking/soju.nix
@@ -120,5 +120,5 @@ in
     };
   };
 
-  meta.maintainers = with maintainers; [ malvo ];
+  meta.maintainers = with maintainers; [ malte-v ];
 }
diff --git a/nixos/modules/services/networking/ssh/lshd.nix b/nixos/modules/services/networking/ssh/lshd.nix
index 41c4ec2d295..af64969c2fc 100644
--- a/nixos/modules/services/networking/ssh/lshd.nix
+++ b/nixos/modules/services/networking/ssh/lshd.nix
@@ -40,7 +40,7 @@ in
         type = types.listOf types.str;
         description = lib.mdDoc ''
           List of network interfaces where listening for connections.
-          When providing the empty list, `[]', lshd listens on all
+          When providing the empty list, `[]`, lshd listens on all
           network interfaces.
         '';
         example = [ "localhost" "1.2.3.4:443" ];
@@ -169,11 +169,11 @@ in
             else (concatStrings (map (i: "--interface=\"${i}\"")
                                      interfaces))} \
           -h "${hostKey}" \
-          ${if !syslog then "--no-syslog" else ""} \
+          ${optionalString (!syslog) "--no-syslog" } \
           ${if passwordAuthentication then "--password" else "--no-password" } \
           ${if publicKeyAuthentication then "--publickey" else "--no-publickey" } \
           ${if rootLogin then "--root-login" else "--no-root-login" } \
-          ${if loginShell != null then "--login-shell=\"${loginShell}\"" else "" } \
+          ${optionalString (loginShell != null) "--login-shell=\"${loginShell}\"" } \
           ${if srpKeyExchange then "--srp-keyexchange" else "--no-srp-keyexchange" } \
           ${if !tcpForwarding then "--no-tcpip-forward" else "--tcpip-forward"} \
           ${if x11Forwarding then "--x11-forward" else "--no-x11-forward" } \
diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix
index af8200c7e29..e75239e059d 100644
--- a/nixos/modules/services/networking/ssh/sshd.nix
+++ b/nixos/modules/services/networking/ssh/sshd.nix
@@ -12,8 +12,24 @@ let
     then cfgc.package
     else pkgs.buildPackages.openssh;
 
+  # reports boolean as yes / no
+  mkValueStringSshd = with lib; v:
+        if isInt           v then toString v
+        else if isString   v then v
+        else if true  ==   v then "yes"
+        else if false ==   v then "no"
+        else if isList     v then concatStringsSep "," v
+        else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}";
+
+  # dont use the "=" operator
+  settingsFormat = (pkgs.formats.keyValue {
+      mkKeyValue = lib.generators.mkKeyValueDefault {
+      mkValueString = mkValueStringSshd;
+    } " ";});
+
+  configFile = settingsFormat.generate "config" cfg.settings;
   sshconf = pkgs.runCommand "sshd.conf-validated" { nativeBuildInputs = [ validationPackage ]; } ''
-    cat >$out <<EOL
+    cat ${configFile} - >$out <<EOL
     ${cfg.extraConfig}
     EOL
 
@@ -24,6 +40,7 @@ let
   cfg  = config.services.openssh;
   cfgc = config.programs.ssh;
 
+
   nssModulesPath = config.system.nssModules.path;
 
   userOptions = {
@@ -79,9 +96,20 @@ in
 
 {
   imports = [
-    (mkAliasOptionModule [ "services" "sshd" "enable" ] [ "services" "openssh" "enable" ])
-    (mkAliasOptionModule [ "services" "openssh" "knownHosts" ] [ "programs" "ssh" "knownHosts" ])
+    (mkAliasOptionModuleMD [ "services" "sshd" "enable" ] [ "services" "openssh" "enable" ])
+    (mkAliasOptionModuleMD [ "services" "openssh" "knownHosts" ] [ "programs" "ssh" "knownHosts" ])
     (mkRenamedOptionModule [ "services" "openssh" "challengeResponseAuthentication" ] [ "services" "openssh" "kbdInteractiveAuthentication" ])
+
+    (mkRenamedOptionModule [ "services" "openssh" "kbdInteractiveAuthentication" ] [  "services" "openssh" "settings" "KbdInteractiveAuthentication" ])
+    (mkRenamedOptionModule [ "services" "openssh" "passwordAuthentication" ] [  "services" "openssh" "settings" "PasswordAuthentication" ])
+    (mkRenamedOptionModule [ "services" "openssh" "useDns" ] [  "services" "openssh" "settings" "UseDns" ])
+    (mkRenamedOptionModule [ "services" "openssh" "permitRootLogin" ] [  "services" "openssh" "settings" "PermitRootLogin" ])
+    (mkRenamedOptionModule [ "services" "openssh" "logLevel" ] [  "services" "openssh" "settings" "LogLevel" ])
+    (mkRenamedOptionModule [ "services" "openssh" "macs" ] [  "services" "openssh" "settings" "Macs" ])
+    (mkRenamedOptionModule [ "services" "openssh" "ciphers" ] [  "services" "openssh" "settings" "Ciphers" ])
+    (mkRenamedOptionModule [ "services" "openssh" "kexAlgorithms" ] [  "services" "openssh" "settings" "KexAlgorithms" ])
+    (mkRenamedOptionModule [ "services" "openssh" "gatewayPorts" ] [  "services" "openssh" "settings" "GatewayPorts" ])
+    (mkRenamedOptionModule [ "services" "openssh" "forwardX11" ] [  "services" "openssh" "settings" "X11Forwarding" ])
   ];
 
   ###### interface
@@ -109,14 +137,6 @@ in
         '';
       };
 
-      forwardX11 = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to allow X11 connections to be forwarded.
-        '';
-      };
-
       allowSFTP = mkOption {
         type = types.bool;
         default = true;
@@ -145,24 +165,6 @@ in
         '';
       };
 
-      permitRootLogin = mkOption {
-        default = "prohibit-password";
-        type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"];
-        description = lib.mdDoc ''
-          Whether the root user can login using ssh.
-        '';
-      };
-
-      gatewayPorts = mkOption {
-        type = types.str;
-        default = "no";
-        description = lib.mdDoc ''
-          Specifies whether remote hosts are allowed to connect to
-          ports forwarded for the client.  See
-          {manpage}`sshd_config(5)`.
-        '';
-      };
-
       ports = mkOption {
         type = types.listOf types.port;
         default = [22];
@@ -210,22 +212,6 @@ in
         '';
       };
 
-      passwordAuthentication = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Specifies whether password authentication is allowed.
-        '';
-      };
-
-      kbdInteractiveAuthentication = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Specifies whether keyboard-interactive authentication is allowed.
-        '';
-      };
-
       hostKeys = mkOption {
         type = types.listOf types.attrs;
         default =
@@ -288,84 +274,137 @@ in
         '';
       };
 
-      kexAlgorithms = mkOption {
-        type = types.listOf types.str;
-        default = [
-          "sntrup761x25519-sha512@openssh.com"
-          "curve25519-sha256"
-          "curve25519-sha256@libssh.org"
-          "diffie-hellman-group-exchange-sha256"
-        ];
-        description = lib.mdDoc ''
-          Allowed key exchange algorithms
-
-          Uses the lower bound recommended in both
-          <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
-          and
-          <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
-        '';
-      };
 
-      ciphers = mkOption {
-        type = types.listOf types.str;
-        default = [
-          "chacha20-poly1305@openssh.com"
-          "aes256-gcm@openssh.com"
-          "aes128-gcm@openssh.com"
-          "aes256-ctr"
-          "aes192-ctr"
-          "aes128-ctr"
-        ];
-        description = lib.mdDoc ''
-          Allowed ciphers
 
-          Defaults to recommended settings from both
-          <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
-          and
-          <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
+      settings = mkOption {
+        description = lib.mdDoc "Configuration for `sshd_config(5)`.";
+        default = { };
+        example = literalExpression ''
+          {
+            UseDns = true;
+            PasswordAuthentication = false;
+          }
         '';
-      };
-
-      macs = mkOption {
-        type = types.listOf types.str;
-        default = [
-          "hmac-sha2-512-etm@openssh.com"
-          "hmac-sha2-256-etm@openssh.com"
-          "umac-128-etm@openssh.com"
-          "hmac-sha2-512"
-          "hmac-sha2-256"
-          "umac-128@openssh.com"
-        ];
-        description = lib.mdDoc ''
-          Allowed MACs
+        type = types.submodule ({name, ...}: {
+          freeformType = settingsFormat.type;
+          options = {
+            LogLevel = mkOption {
+              type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ];
+              default = "INFO"; # upstream default
+              description = lib.mdDoc ''
+                Gives the verbosity level that is used when logging messages from sshd(8). Logging with a DEBUG level
+                violates the privacy of users and is not recommended.
+              '';
+            };
+            UseDns = mkOption {
+              type = types.bool;
+              # apply if cfg.useDns then "yes" else "no"
+              default = false;
+              description = lib.mdDoc ''
+                Specifies whether sshd(8) should look up the remote host name, and to check that the resolved host name for
+                the remote IP address maps back to the very same IP address.
+                If this option is set to no (the default) then only addresses and not host names may be used in
+                ~/.ssh/authorized_keys from and sshd_config Match Host directives.
+              '';
+            };
+            X11Forwarding = mkOption {
+              type = types.bool;
+              default = false;
+              description = lib.mdDoc ''
+                Whether to allow X11 connections to be forwarded.
+              '';
+            };
+            PasswordAuthentication = mkOption {
+              type = types.bool;
+              default = true;
+              description = lib.mdDoc ''
+                Specifies whether password authentication is allowed.
+              '';
+            };
+            PermitRootLogin = mkOption {
+              default = "prohibit-password";
+              type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"];
+              description = lib.mdDoc ''
+                Whether the root user can login using ssh.
+              '';
+            };
+            KbdInteractiveAuthentication = mkOption {
+              type = types.bool;
+              default = true;
+              description = lib.mdDoc ''
+                Specifies whether keyboard-interactive authentication is allowed.
+              '';
+            };
+            GatewayPorts = mkOption {
+              type = types.str;
+              default = "no";
+              description = lib.mdDoc ''
+                Specifies whether remote hosts are allowed to connect to
+                ports forwarded for the client.  See
+                {manpage}`sshd_config(5)`.
+              '';
+            };
+            KexAlgorithms = mkOption {
+              type = types.listOf types.str;
+              default = [
+                "sntrup761x25519-sha512@openssh.com"
+                "curve25519-sha256"
+                "curve25519-sha256@libssh.org"
+                "diffie-hellman-group-exchange-sha256"
+              ];
+              description = lib.mdDoc ''
+                Allowed key exchange algorithms
 
-          Defaults to recommended settings from both
-          <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
-          and
-          <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
-        '';
-      };
+                Uses the lower bound recommended in both
+                <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
+                and
+                <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
+              '';
+            };
+            Macs = mkOption {
+              type = types.listOf types.str;
+              default = [
+                "hmac-sha2-512-etm@openssh.com"
+                "hmac-sha2-256-etm@openssh.com"
+                "umac-128-etm@openssh.com"
+              ];
+              description = lib.mdDoc ''
+                Allowed MACs
 
-      logLevel = mkOption {
-        type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ];
-        default = "INFO"; # upstream default
-        description = lib.mdDoc ''
-          Gives the verbosity level that is used when logging messages from sshd(8). The possible values are:
-          QUIET, FATAL, ERROR, INFO, VERBOSE, DEBUG, DEBUG1, DEBUG2, and DEBUG3. The default is INFO. DEBUG and DEBUG1
-          are equivalent. DEBUG2 and DEBUG3 each specify higher levels of debugging output. Logging with a DEBUG level
-          violates the privacy of users and is not recommended.
-        '';
-      };
+                Defaults to recommended settings from both
+                <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
+                and
+                <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
+              '';
+            };
+            StrictModes = mkOption {
+              type = types.bool;
+              default = true;
+              description = lib.mdDoc ''
+                Whether sshd should check file modes and ownership of directories
+              '';
+            };
+            Ciphers = mkOption {
+              type = types.listOf types.str;
+              default = [
+                "chacha20-poly1305@openssh.com"
+                "aes256-gcm@openssh.com"
+                "aes128-gcm@openssh.com"
+                "aes256-ctr"
+                "aes192-ctr"
+                "aes128-ctr"
+              ];
+              description = lib.mdDoc ''
+                Allowed ciphers
 
-      useDns = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Specifies whether sshd(8) should look up the remote host name, and to check that the resolved host name for
-          the remote IP address maps back to the very same IP address.
-          If this option is set to no (the default) then only addresses and not host names may be used in
-          ~/.ssh/authorized_keys from and sshd_config Match Host directives.
-        '';
+                Defaults to recommended settings from both
+                <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
+                and
+                <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
+              '';
+            };
+          };
+        });
       };
 
       extraConfig = mkOption {
@@ -441,10 +480,10 @@ in
                       mkdir -m 0755 -p "$(dirname '${k.path}')"
                       ssh-keygen \
                         -t "${k.type}" \
-                        ${if k ? bits then "-b ${toString k.bits}" else ""} \
-                        ${if k ? rounds then "-a ${toString k.rounds}" else ""} \
-                        ${if k ? comment then "-C '${k.comment}'" else ""} \
-                        ${if k ? openSSHFormat && k.openSSHFormat then "-o" else ""} \
+                        ${optionalString (k ? bits) "-b ${toString k.bits}"} \
+                        ${optionalString (k ? rounds) "-a ${toString k.rounds}"} \
+                        ${optionalString (k ? comment) "-C '${k.comment}'"} \
+                        ${optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \
                         -f "${k.path}" \
                         -N ""
                   fi
@@ -491,19 +530,19 @@ in
 
       };
 
-    networking.firewall.allowedTCPPorts = if cfg.openFirewall then cfg.ports else [];
+    networking.firewall.allowedTCPPorts = optionals cfg.openFirewall cfg.ports;
 
     security.pam.services.sshd =
       { startSession = true;
         showMotd = true;
-        unixAuth = cfg.passwordAuthentication;
+        unixAuth = cfg.settings.PasswordAuthentication;
       };
 
     # These values are merged with the ones defined externally, see:
     # https://github.com/NixOS/nixpkgs/pull/10155
     # https://github.com/NixOS/nixpkgs/pull/41745
     services.openssh.authorizedKeysFiles =
-      [ "%h/.ssh/authorized_keys" "%h/.ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
+      [ "%h/.ssh/authorized_keys" "/etc/ssh/authorized_keys.d/%u" ];
 
     services.openssh.extraConfig = mkOrder 0
       ''
@@ -517,26 +556,16 @@ in
         '') cfg.ports}
 
         ${concatMapStrings ({ port, addr, ... }: ''
-          ListenAddress ${addr}${if port != null then ":" + toString port else ""}
+          ListenAddress ${addr}${optionalString (port != null) (":" + toString port)}
         '') cfg.listenAddresses}
 
         ${optionalString cfgc.setXAuthLocation ''
             XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
         ''}
-
-        X11Forwarding ${if cfg.forwardX11 then "yes" else "no"}
-
         ${optionalString cfg.allowSFTP ''
           Subsystem sftp ${cfg.sftpServerExecutable} ${concatStringsSep " " cfg.sftpFlags}
         ''}
-
-        PermitRootLogin ${cfg.permitRootLogin}
-        GatewayPorts ${cfg.gatewayPorts}
-        PasswordAuthentication ${if cfg.passwordAuthentication then "yes" else "no"}
-        KbdInteractiveAuthentication ${if cfg.kbdInteractiveAuthentication then "yes" else "no"}
-
         PrintMotd no # handled by pam_motd
-
         AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}
         ${optionalString (cfg.authorizedKeysCommand != "none") ''
           AuthorizedKeysCommand ${cfg.authorizedKeysCommand}
@@ -546,24 +575,30 @@ in
         ${flip concatMapStrings cfg.hostKeys (k: ''
           HostKey ${k.path}
         '')}
-
-        KexAlgorithms ${concatStringsSep "," cfg.kexAlgorithms}
-        Ciphers ${concatStringsSep "," cfg.ciphers}
-        MACs ${concatStringsSep "," cfg.macs}
-
-        LogLevel ${cfg.logLevel}
-
-        UseDNS ${if cfg.useDns then "yes" else "no"}
-
       '';
 
-    assertions = [{ assertion = if cfg.forwardX11 then cfgc.setXAuthLocation else true;
-                    message = "cannot enable X11 forwarding without setting xauth location";}]
+    assertions = [{ assertion = if cfg.settings.X11Forwarding then cfgc.setXAuthLocation else true;
+                    message = "cannot enable X11 forwarding without setting xauth location";}
+                  (let
+                    duplicates =
+                      # Filter out the groups with more than 1 element
+                      lib.filter (l: lib.length l > 1) (
+                        # Grab the groups, we don't care about the group identifiers
+                        lib.attrValues (
+                          # Group the settings that are the same in lower case
+                          lib.groupBy lib.strings.toLower (attrNames cfg.settings)
+                        )
+                      );
+                    formattedDuplicates = lib.concatMapStringsSep ", " (dupl: "(${lib.concatStringsSep ", " dupl})") duplicates;
+                  in
+                  {
+                    assertion = lib.length duplicates == 0;
+                    message = ''Duplicate sshd config key; does your capitalization match the option's? Duplicate keys: ${formattedDuplicates}'';
+                  })]
       ++ forEach cfg.listenAddresses ({ addr, ... }: {
         assertion = addr != null;
         message = "addr must be specified in each listenAddresses entry";
       });
-
   };
 
 }
diff --git a/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix b/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
index 84ac4fef26e..1ad5fdbcef0 100644
--- a/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
+++ b/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
@@ -225,20 +225,22 @@ in {
       irrespective of the value of this option (even when set to no).
     '';
 
-    childless = mkEnumParam [ "allow" "force" "never" ] "allow" ''
-      Use childless IKE_SA initiation (RFC 6023) for IKEv2.  Acceptable values
-      are `allow` (the default), `force` and
-      `never`. If set to `allow`, responders
+    childless = mkEnumParam [ "allow" "prefer" "force" "never" ] "allow" ''
+      Use childless IKE_SA initiation (_allow_, _prefer_, _force_ or _never_).
+
+      Use childless IKE_SA initiation (RFC 6023) for IKEv2, with the first
+      CHILD_SA created with a separate CREATE_CHILD_SA exchange (e.g. to use an
+      independent DH exchange for all CHILD_SAs).  Acceptable values are `allow`
+      (the default), `prefer`, `force` and `never`. If set to `allow`, responders
       will accept childless IKE_SAs (as indicated via notify in the IKE_SA_INIT
-      response) while initiators continue to create regular IKE_SAs with the
-      first CHILD_SA created during IKE_AUTH, unless the IKE_SA is initiated
-      explicitly without any children (which will fail if the responder does not
-      support or has disabled this extension).  If set to
-      `force`, only childless initiation is accepted and the
-      first CHILD_SA is created with a separate CREATE_CHILD_SA exchange
-      (e.g. to use an independent DH exchange for all CHILD_SAs). Finally,
-      setting the option to `never` disables support for
-      childless IKE_SAs as responder.
+      response) while initiators continue to create regular IKE_SAs with the first
+      CHILD_SA created during IKE_AUTH, unless the IKE_SA is initiated explicitly
+      without any children (which will fail if the responder does not support or
+      has disabled this extension). The effect of `prefer` is the same as `allow`
+      on responders, but as initiator a childless IKE_SA is initiated if the
+      responder supports it. If set to `force`, only childless initiation is
+      accepted in either role.  Finally, setting the option to `never` disables
+      support for childless IKE_SAs as responder.
     '';
 
     send_certreq = mkYesNoParam yes ''
@@ -357,11 +359,22 @@ in {
     if_id_in = mkStrParam "0" ''
       XFRM interface ID set on inbound policies/SA, can be overridden by child
       config, see there for details.
+
+      The special value `%unique` allocates a unique interface ID per IKE_SA,
+      which is inherited by all its CHILD_SAs (unless overridden there), beyond
+      that the value `%unique-dir` assigns a different unique interface ID for
+      each direction (in/out).
+
     '';
 
     if_id_out = mkStrParam "0" ''
       XFRM interface ID set on outbound policies/SA, can be overridden by child
       config, see there for details.
+
+      The special value `%unique` allocates a unique interface ID per IKE_SA,
+      which is inherited by all its CHILD_SAs (unless overridden there), beyond
+      that the value `%unique-dir` assigns a different unique interface ID for
+      each direction (in/out).
     '';
 
     mediation = mkYesNoParam no ''
@@ -985,12 +998,14 @@ in {
         protection.
       '';
 
-      hw_offload = mkEnumParam ["yes" "no" "auto"] "no" ''
+      hw_offload = mkEnumParam ["yes" "no" "auto" "crypto" "packet"] "no" ''
         Enable hardware offload for this CHILD_SA, if supported by the IPsec
-        implementation. The value `yes` enforces offloading
-        and the installation will fail if it's not supported by either kernel or
-        device. The value `auto` enables offloading, if it's
-        supported, but the installation does not fail otherwise.
+        implementation. The values `crypto` or `packet` enforce crypto or full
+        packet offloading and the installation will fail if the selected mode is not
+        supported by either kernel or device. On Linux, `packet` also offloads
+        policies, including trap policies. The value `auto` enables full packet
+        or crypto offloading, if either is supported, but the installation does not
+        fail otherwise.
       '';
 
       copy_df = mkYesNoParam yes ''
diff --git a/nixos/modules/services/networking/strongswan.nix b/nixos/modules/services/networking/strongswan.nix
index 8b1398bfd47..e58526814d1 100644
--- a/nixos/modules/services/networking/strongswan.nix
+++ b/nixos/modules/services/networking/strongswan.nix
@@ -4,7 +4,7 @@ let
 
   inherit (builtins) toFile;
   inherit (lib) concatMapStringsSep concatStringsSep mapAttrsToList
-                mkIf mkEnableOption mkOption types literalExpression;
+                mkIf mkEnableOption mkOption types literalExpression optionalString;
 
   cfg = config.services.strongswan;
 
@@ -34,8 +34,8 @@ let
 
   strongswanConf = {setup, connections, ca, secretsFile, managePlugins, enabledPlugins}: toFile "strongswan.conf" ''
     charon {
-      ${if managePlugins then "load_modular = no" else ""}
-      ${if managePlugins then ("load = " + (concatStringsSep " " enabledPlugins)) else ""}
+      ${optionalString managePlugins "load_modular = no"}
+      ${optionalString managePlugins ("load = " + (concatStringsSep " " enabledPlugins))}
       plugins {
         stroke {
           secrets_file = ${secretsFile}
diff --git a/nixos/modules/services/networking/stunnel.nix b/nixos/modules/services/networking/stunnel.nix
index 4f592fb312d..996e9b22539 100644
--- a/nixos/modules/services/networking/stunnel.nix
+++ b/nixos/modules/services/networking/stunnel.nix
@@ -154,8 +154,8 @@ in
     environment.systemPackages = [ pkgs.stunnel ];
 
     environment.etc."stunnel.cfg".text = ''
-      ${ if cfg.user != null then "setuid = ${cfg.user}" else "" }
-      ${ if cfg.group != null then "setgid = ${cfg.group}" else "" }
+      ${ optionalString (cfg.user != null) "setuid = ${cfg.user}" }
+      ${ optionalString (cfg.group != null) "setgid = ${cfg.group}" }
 
       debug = ${cfg.logLevel}
 
diff --git a/nixos/modules/services/networking/syncplay.nix b/nixos/modules/services/networking/syncplay.nix
index 726f6567107..0a66d93bf15 100644
--- a/nixos/modules/services/networking/syncplay.nix
+++ b/nixos/modules/services/networking/syncplay.nix
@@ -8,7 +8,8 @@ let
   cmdArgs =
     [ "--port" cfg.port ]
     ++ optionals (cfg.salt != null) [ "--salt" cfg.salt ]
-    ++ optionals (cfg.certDir != null) [ "--tls" cfg.certDir ];
+    ++ optionals (cfg.certDir != null) [ "--tls" cfg.certDir ]
+    ++ cfg.extraArgs;
 
 in
 {
@@ -33,7 +34,22 @@ in
         default = null;
         description = lib.mdDoc ''
           Salt to allow room operator passwords generated by this server
-          instance to still work when the server is restarted.
+          instance to still work when the server is restarted.  The salt will be
+          readable in the nix store and the processlist.  If this is not
+          intended use `saltFile` instead.  Mutually exclusive with
+          <option>services.syncplay.saltFile</option>.
+        '';
+      };
+
+      saltFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = lib.mdDoc ''
+          Path to the file that contains the server salt.  This allows room
+          operator passwords generated by this server instance to still work
+          when the server is restarted.  `null`, the server doesn't load the
+          salt from a file.  Mutually exclusive with
+          <option>services.syncplay.salt</option>.
         '';
       };
 
@@ -46,6 +62,14 @@ in
         '';
       };
 
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = lib.mdDoc ''
+          Additional arguments to be passed to the service.
+        '';
+      };
+
       user = mkOption {
         type = types.str;
         default = "nobody";
@@ -74,21 +98,31 @@ in
   };
 
   config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.salt == null || cfg.saltFile == null;
+        message = "services.syncplay.salt and services.syncplay.saltFile are mutually exclusive.";
+      }
+    ];
     systemd.services.syncplay = {
       description = "Syncplay Service";
-      wantedBy    = [ "multi-user.target" ];
-      after       = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
 
       serviceConfig = {
         User = cfg.user;
         Group = cfg.group;
-        LoadCredential = lib.mkIf (cfg.passwordFile != null) "password:${cfg.passwordFile}";
+        LoadCredential = lib.optional (cfg.passwordFile != null) "password:${cfg.passwordFile}"
+          ++ lib.optional (cfg.saltFile != null) "salt:${cfg.saltFile}";
       };
 
       script = ''
         ${lib.optionalString (cfg.passwordFile != null) ''
           export SYNCPLAY_PASSWORD=$(cat "''${CREDENTIALS_DIRECTORY}/password")
         ''}
+        ${lib.optionalString (cfg.saltFile != null) ''
+          export SYNCPLAY_SALT=$(cat "''${CREDENTIALS_DIRECTORY}/salt")
+        ''}
         exec ${pkgs.syncplay-nogui}/bin/syncplay-server ${escapeShellArgs cmdArgs}
       '';
     };
diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
index adbb25ccb9b..69b45eb02d1 100644
--- a/nixos/modules/services/networking/syncthing.nix
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -55,9 +55,9 @@ 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.devices == {} || ! cfg.overrideDevices) " + .devices"}),
-        "folders": (${builtins.toJSON folders}${optionalString (cfg.folders == {} || ! cfg.overrideFolders) " + .folders"})
-    } * ${builtins.toJSON cfg.extraOptions}')
+        "devices": ('${escapeShellArg (builtins.toJSON devices)}'${optionalString (cfg.devices == {} || ! cfg.overrideDevices) " + .devices"}),
+        "folders": ('${escapeShellArg (builtins.toJSON folders)}'${optionalString (cfg.folders == {} || ! cfg.overrideFolders) " + .folders"})
+    } * '${escapeShellArg (builtins.toJSON cfg.extraOptions)})
 
     # send the new config
     curl -X PUT -d "$new_cfg" ${cfg.guiAddress}/rest/config
@@ -384,6 +384,29 @@ in {
         description = mdDoc ''
           Extra configuration options for Syncthing.
           See <https://docs.syncthing.net/users/config.html>.
+          Note that this attribute set does not exactly match the documented
+          xml format. Instead, this is the format of the json rest api. There
+          are slight differences. For example, this xml:
+          ```xml
+          <options>
+            <listenAddress>default</listenAddress>
+            <minHomeDiskFree unit="%">1</minHomeDiskFree>
+          </options>
+          ```
+          corresponds to the json:
+          ```json
+          {
+            options: {
+              listenAddresses = [
+                "default"
+              ];
+              minHomeDiskFree = {
+                unit = "%";
+                value = 1;
+              };
+            };
+          }
+          ```
         '';
         example = {
           options.localAnnounceEnabled = false;
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
index 233bfdf9ebf..6eeee71e834 100644
--- a/nixos/modules/services/networking/tailscale.nix
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -59,7 +59,8 @@ in {
       path = [
         config.networking.resolvconf.package # for configuring DNS in some configs
         pkgs.procps     # for collecting running services (opt-in feature)
-        pkgs.glibc      # for `getent` to look up user shells
+        pkgs.getent     # for `getent` to look up user shells
+        pkgs.kmod       # required to pass tailscale's v6nat check
       ];
       serviceConfig.Environment = [
         "PORT=${toString cfg.port}"
@@ -82,8 +83,8 @@ in {
     };
 
     boot.kernel.sysctl = mkIf (cfg.useRoutingFeatures == "server" || cfg.useRoutingFeatures == "both") {
-      "net.ipv4.conf.all.forwarding" = mkDefault true;
-      "net.ipv6.conf.all.forwarding" = mkDefault true;
+      "net.ipv4.conf.all.forwarding" = mkOverride 97 true;
+      "net.ipv6.conf.all.forwarding" = mkOverride 97 true;
     };
 
     networking.firewall.checkReversePath = mkIf (cfg.useRoutingFeatures == "client" || cfg.useRoutingFeatures == "both") "loose";
diff --git a/nixos/modules/services/networking/teleport.nix b/nixos/modules/services/networking/teleport.nix
index 6433554f87d..399af711c0e 100644
--- a/nixos/modules/services/networking/teleport.nix
+++ b/nixos/modules/services/networking/teleport.nix
@@ -11,6 +11,14 @@ in
     services.teleport = with lib.types; {
       enable = mkEnableOption (lib.mdDoc "the Teleport service");
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.teleport;
+        defaultText = lib.literalMD "pkgs.teleport";
+        example = lib.literalMD "pkgs.teleport_11";
+        description = lib.mdDoc "The teleport package to use";
+      };
+
       settings = mkOption {
         type = settingsYaml.type;
         default = { };
@@ -74,14 +82,14 @@ in
   };
 
   config = mkIf config.services.teleport.enable {
-    environment.systemPackages = [ pkgs.teleport ];
+    environment.systemPackages = [ cfg.package ];
 
     systemd.services.teleport = {
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.teleport}/bin/teleport start \
+          ${cfg.package}/bin/teleport start \
             ${optionalString cfg.insecure.enable "--insecure"} \
             ${optionalString cfg.diag.enable "--diag-addr=${cfg.diag.addr}:${toString cfg.diag.port}"} \
             ${optionalString (cfg.settings != { }) "--config=${settingsYaml.generate "teleport.yaml" cfg.settings}"}
diff --git a/nixos/modules/services/networking/thelounge.nix b/nixos/modules/services/networking/thelounge.nix
index a188ffe866b..321e46fb5d4 100644
--- a/nixos/modules/services/networking/thelounge.nix
+++ b/nixos/modules/services/networking/thelounge.nix
@@ -25,6 +25,8 @@ in
   options.services.thelounge = {
     enable = mkEnableOption (lib.mdDoc "The Lounge web IRC client");
 
+    package = mkPackageOptionMD pkgs "thelounge" { };
+
     public = mkOption {
       type = types.bool;
       default = false;
@@ -46,14 +48,16 @@ in
     extraConfig = mkOption {
       default = { };
       type = types.attrs;
-      example = literalExpression ''{
-        reverseProxy = true;
-        defaults = {
-          name = "Your Network";
-          host = "localhost";
-          port = 6697;
-        };
-      }'';
+      example = literalExpression ''
+        {
+          reverseProxy = true;
+          defaults = {
+            name = "Your Network";
+            host = "localhost";
+            port = 6697;
+          };
+        }
+      '';
       description = lib.mdDoc ''
         The Lounge's {file}`config.js` contents as attribute set (will be
         converted to JSON to generate the configuration file).
@@ -93,11 +97,11 @@ in
       serviceConfig = {
         User = "thelounge";
         StateDirectory = baseNameOf dataDir;
-        ExecStart = "${pkgs.thelounge}/bin/thelounge start";
+        ExecStart = "${getExe cfg.package} start";
       };
     };
 
-    environment.systemPackages = [ pkgs.thelounge ];
+    environment.systemPackages = [ cfg.package ];
   };
 
   meta = {
diff --git a/nixos/modules/services/networking/tmate-ssh-server.nix b/nixos/modules/services/networking/tmate-ssh-server.nix
index f7740b1ddfc..ff4ce077330 100644
--- a/nixos/modules/services/networking/tmate-ssh-server.nix
+++ b/nixos/modules/services/networking/tmate-ssh-server.nix
@@ -28,7 +28,7 @@ in
     host = mkOption {
       type = types.str;
       description = mdDoc "External host name";
-      defaultText = lib.literalExpression "config.networking.domain or config.networking.hostName ";
+      defaultText = lib.literalExpression "config.networking.domain or config.networking.hostName";
       default =
         if domain == null then
           config.networking.hostName
diff --git a/nixos/modules/services/networking/twingate.nix b/nixos/modules/services/networking/twingate.nix
index 17140bffd21..1454a7431cd 100644
--- a/nixos/modules/services/networking/twingate.nix
+++ b/nixos/modules/services/networking/twingate.nix
@@ -1,28 +1,24 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   cfg = config.services.twingate;
-
-in {
-
+in
+{
   options.services.twingate = {
-    enable = mkEnableOption (lib.mdDoc "Twingate Client daemon");
+    enable = lib.mkEnableOption (lib.mdDoc "Twingate Client daemon");
+    package = lib.mkPackageOptionMD pkgs "twingate" { };
   };
 
-  config = mkIf cfg.enable {
-
-    networking.firewall.checkReversePath = lib.mkDefault false;
-    networking.networkmanager.enable = true;
-
-    environment.systemPackages = [ pkgs.twingate ]; # for the CLI
-    systemd.packages = [ pkgs.twingate ];
+  config = lib.mkIf cfg.enable {
+    systemd.packages = [ cfg.package ];
+    systemd.services.twingate = {
+      preStart = "cp -r -n ${cfg.package}/etc/twingate/. /etc/twingate/";
+      wantedBy = [ "multi-user.target" ];
+    };
 
-    systemd.services.twingate.preStart = ''
-      cp -r -n ${pkgs.twingate}/etc/twingate/. /etc/twingate/
-    '';
+    networking.firewall.checkReversePath = lib.mkDefault "loose";
+    services.resolved.enable = !(config.networking.networkmanager.enable);
 
-    systemd.services.twingate.wantedBy = [ "multi-user.target" ];
+    environment.systemPackages = [ cfg.package ]; # For the CLI.
   };
 }
diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix
index c85dd03867f..0426dbb0c83 100644
--- a/nixos/modules/services/networking/unbound.nix
+++ b/nixos/modules/services/networking/unbound.nix
@@ -286,6 +286,8 @@ in {
         LockPersonality = true;
         RestrictSUIDSGID = true;
 
+        ReadWritePaths = [ cfg.stateDir ];
+
         Restart = "on-failure";
         RestartSec = "5s";
       };
diff --git a/nixos/modules/services/networking/unifi.nix b/nixos/modules/services/networking/unifi.nix
index d220aa9fbbe..3579d67aa54 100644
--- a/nixos/modules/services/networking/unifi.nix
+++ b/nixos/modules/services/networking/unifi.nix
@@ -33,8 +33,8 @@ in
 
     services.unifi.unifiPackage = mkOption {
       type = types.package;
-      default = pkgs.unifiLTS;
-      defaultText = literalExpression "pkgs.unifiLTS";
+      default = pkgs.unifi5;
+      defaultText = literalExpression "pkgs.unifi5";
       description = lib.mdDoc ''
         The unifi package to use.
       '';
@@ -42,10 +42,10 @@ in
 
     services.unifi.mongodbPackage = mkOption {
       type = types.package;
-      default = pkgs.mongodb;
+      default = pkgs.mongodb-4_4;
       defaultText = literalExpression "pkgs.mongodb";
       description = lib.mdDoc ''
-        The mongodb package to use.
+        The mongodb package to use. Please note: unifi7 officially only supports mongodb up until 3.6 but works with 4.4.
       '';
     };
 
@@ -193,6 +193,4 @@ in
     (mkRemovedOptionModule [ "services" "unifi" "dataDir" ] "You should move contents of dataDir to /var/lib/unifi/data" )
     (mkRenamedOptionModule [ "services" "unifi" "openPorts" ] [ "services" "unifi" "openFirewall" ])
   ];
-
-  meta.maintainers = with lib.maintainers; [ erictapen pennae ];
 }
diff --git a/nixos/modules/services/networking/v2raya.nix b/nixos/modules/services/networking/v2raya.nix
index 2d697b4fb56..0bea73798da 100644
--- a/nixos/modules/services/networking/v2raya.nix
+++ b/nixos/modules/services/networking/v2raya.nix
@@ -12,27 +12,38 @@ with lib;
   config = mkIf config.services.v2raya.enable {
     environment.systemPackages = [ pkgs.v2raya ];
 
-    systemd.services.v2raya = {
-      unitConfig = {
-        Description = "v2rayA service";
-        Documentation = "https://github.com/v2rayA/v2rayA/wiki";
-        After = [ "network.target" "nss-lookup.target" "iptables.service" "ip6tables.service" ];
-        Wants = [ "network.target" ];
-      };
+    systemd.services.v2raya =
+      let
+        nftablesEnabled = config.networking.nftables.enable;
+        iptablesServices = [
+          "iptables.service"
+        ] ++ optional config.networking.enableIPv6 "ip6tables.service";
+        tableServices = if nftablesEnabled then [ "nftables.service" ] else iptablesServices;
+      in
+      {
+        unitConfig = {
+          Description = "v2rayA service";
+          Documentation = "https://github.com/v2rayA/v2rayA/wiki";
+          After = [
+            "network.target"
+            "nss-lookup.target"
+          ] ++ tableServices;
+          Wants = [ "network.target" ];
+        };
 
-      serviceConfig = {
-        User = "root";
-        ExecStart = "${getExe pkgs.v2raya} --log-disable-timestamp";
-        Environment = [ "V2RAYA_LOG_FILE=/var/log/v2raya/v2raya.log" ];
-        LimitNPROC = 500;
-        LimitNOFILE = 1000000;
-        Restart = "on-failure";
-        Type = "simple";
-      };
+        serviceConfig = {
+          User = "root";
+          ExecStart = "${getExe pkgs.v2raya} --log-disable-timestamp";
+          Environment = [ "V2RAYA_LOG_FILE=/var/log/v2raya/v2raya.log" ];
+          LimitNPROC = 500;
+          LimitNOFILE = 1000000;
+          Restart = "on-failure";
+          Type = "simple";
+        };
 
-      wantedBy = [ "multi-user.target" ];
-      path = with pkgs; [ iptables bash iproute2 ]; # required by v2rayA TProxy functionality
-    };
+        wantedBy = [ "multi-user.target" ];
+        path = with pkgs; [ iptables bash iproute2 ]; # required by v2rayA TProxy functionality
+      };
   };
 
   meta.maintainers = with maintainers; [ elliot ];
diff --git a/nixos/modules/services/networking/vdirsyncer.nix b/nixos/modules/services/networking/vdirsyncer.nix
index 6a069943434..f9b880c763e 100644
--- a/nixos/modules/services/networking/vdirsyncer.nix
+++ b/nixos/modules/services/networking/vdirsyncer.nix
@@ -71,7 +71,7 @@ in
     services.vdirsyncer = {
       enable = mkEnableOption (mdDoc "vdirsyncer");
 
-      package = mkPackageOption pkgs "vdirsyncer" {};
+      package = mkPackageOptionMD pkgs "vdirsyncer" {};
 
       jobs = mkOption {
         description = mdDoc "vdirsyncer job configurations";
diff --git a/nixos/modules/services/networking/vsftpd.nix b/nixos/modules/services/networking/vsftpd.nix
index b1f0f740324..318ceb4e509 100644
--- a/nixos/modules/services/networking/vsftpd.nix
+++ b/nixos/modules/services/networking/vsftpd.nix
@@ -305,7 +305,7 @@ in
 
     # If you really have to access root via FTP use mkOverride or userlistDeny
     # = false and whitelist root
-    services.vsftpd.userlist = if cfg.userlistDeny then ["root"] else [];
+    services.vsftpd.userlist = optional cfg.userlistDeny "root";
 
     systemd = {
       tmpfiles.rules = optional cfg.anonymousUser
diff --git a/nixos/modules/services/networking/webhook.nix b/nixos/modules/services/networking/webhook.nix
index b020db6961c..2a78491941c 100644
--- a/nixos/modules/services/networking/webhook.nix
+++ b/nixos/modules/services/networking/webhook.nix
@@ -36,7 +36,7 @@ in {
         which execute configured commands for any person or service that knows the URL
       '');
 
-      package = mkPackageOption pkgs "webhook" {};
+      package = mkPackageOptionMD pkgs "webhook" {};
       user = mkOption {
         type = types.str;
         default = defaultUser;
diff --git a/nixos/modules/services/networking/wgautomesh.nix b/nixos/modules/services/networking/wgautomesh.nix
new file mode 100644
index 00000000000..094281403f7
--- /dev/null
+++ b/nixos/modules/services/networking/wgautomesh.nix
@@ -0,0 +1,163 @@
+{ lib, config, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.wgautomesh;
+  settingsFormat = pkgs.formats.toml { };
+  configFile =
+    # Have to remove nulls manually as TOML generator will not just skip key
+    # if value is null
+    settingsFormat.generate "wgautomesh-config.toml"
+      (filterAttrs (k: v: v != null)
+        (mapAttrs
+          (k: v:
+            if k == "peers"
+            then map (e: filterAttrs (k: v: v != null) e) v
+            else v)
+          cfg.settings));
+  runtimeConfigFile =
+    if cfg.enableGossipEncryption
+    then "/run/wgautomesh/wgautomesh.toml"
+    else configFile;
+in
+{
+  options.services.wgautomesh = {
+    enable = mkEnableOption (mdDoc "the wgautomesh daemon");
+    logLevel = mkOption {
+      type = types.enum [ "trace" "debug" "info" "warn" "error" ];
+      default = "info";
+      description = mdDoc "wgautomesh log level.";
+    };
+    enableGossipEncryption = mkOption {
+      type = types.bool;
+      default = true;
+      description = mdDoc "Enable encryption of gossip traffic.";
+    };
+    gossipSecretFile = mkOption {
+      type = types.path;
+      description = mdDoc ''
+        File containing the gossip secret, a shared secret key to use for gossip
+        encryption.  Required if `enableGossipEncryption` is set.  This file
+        may contain any arbitrary-length utf8 string.  To generate a new gossip
+        secret, use a command such as `openssl rand -base64 32`.
+      '';
+    };
+    enablePersistence = mkOption {
+      type = types.bool;
+      default = true;
+      description = mdDoc "Enable persistence of Wireguard peer info between restarts.";
+    };
+    openFirewall = mkOption {
+      type = types.bool;
+      default = true;
+      description = mdDoc "Automatically open gossip port in firewall (recommended).";
+    };
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+        options = {
+
+          interface = mkOption {
+            type = types.str;
+            description = mdDoc ''
+              Wireguard interface to manage (it is NOT created by wgautomesh, you
+              should use another NixOS option to create it such as
+              `networking.wireguard.interfaces.wg0 = {...};`).
+            '';
+            example = "wg0";
+          };
+          gossip_port = mkOption {
+            type = types.port;
+            description = mdDoc ''
+              wgautomesh gossip port, this MUST be the same number on all nodes in
+              the wgautomesh network.
+            '';
+            default = 1666;
+          };
+          lan_discovery = mkOption {
+            type = types.bool;
+            default = true;
+            description = mdDoc "Enable discovery of peers on the same LAN using UDP broadcast.";
+          };
+          upnp_forward_external_port = mkOption {
+            type = types.nullOr types.port;
+            default = null;
+            description = mdDoc ''
+              Public port number to try to redirect to this machine's Wireguard
+              daemon using UPnP IGD.
+            '';
+          };
+          peers = mkOption {
+            type = types.listOf (types.submodule {
+              options = {
+                pubkey = mkOption {
+                  type = types.str;
+                  description = mdDoc "Wireguard public key of this peer.";
+                };
+                address = mkOption {
+                  type = types.str;
+                  description = mdDoc ''
+                    Wireguard address of this peer (a single IP address, multiple
+                    addresses or address ranges are not supported).
+                  '';
+                  example = "10.0.0.42";
+                };
+                endpoint = mkOption {
+                  type = types.nullOr types.str;
+                  description = mdDoc ''
+                    Bootstrap endpoint for connecting to this Wireguard peer if no
+                    other address is known or none are working.
+                  '';
+                  default = null;
+                  example = "wgnode.mydomain.example:51820";
+                };
+              };
+            });
+            default = [ ];
+            description = mdDoc "wgautomesh peer list.";
+          };
+        };
+
+      };
+      default = { };
+      description = mdDoc "Configuration for wgautomesh.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.wgautomesh.settings = {
+      gossip_secret_file = mkIf cfg.enableGossipEncryption "$CREDENTIALS_DIRECTORY/gossip_secret";
+      persist_file = mkIf cfg.enablePersistence "/var/lib/wgautomesh/state";
+    };
+
+    systemd.services.wgautomesh = {
+      path = [ pkgs.wireguard-tools ];
+      environment = { RUST_LOG = "wgautomesh=${cfg.logLevel}"; };
+      description = "wgautomesh";
+      serviceConfig = {
+        Type = "simple";
+
+        ExecStart = "${getExe pkgs.wgautomesh} ${runtimeConfigFile}";
+        Restart = "always";
+        RestartSec = "30";
+        LoadCredential = mkIf cfg.enableGossipEncryption [ "gossip_secret:${cfg.gossipSecretFile}" ];
+
+        ExecStartPre = mkIf cfg.enableGossipEncryption [
+          ''${pkgs.envsubst}/bin/envsubst \
+              -i ${configFile} \
+              -o ${runtimeConfigFile}''
+        ];
+
+        DynamicUser = true;
+        StateDirectory = "wgautomesh";
+        StateDirectoryMode = "0700";
+        RuntimeDirectory = "wgautomesh";
+        AmbientCapabilities = "CAP_NET_ADMIN";
+        CapabilityBoundingSet = "CAP_NET_ADMIN";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+    networking.firewall.allowedUDPPorts =
+      mkIf cfg.openFirewall [ cfg.settings.gossip_port ];
+  };
+}
+
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index 1d6556f626b..21473388d76 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -170,13 +170,25 @@ let
 
   # peer options
 
-  peerOpts = {
+  peerOpts = self: {
 
     options = {
 
+      name = mkOption {
+        default =
+          replaceStrings
+            [ "/" "-"     " "     "+"     "="     ]
+            [ "-" "\\x2d" "\\x20" "\\x2b" "\\x3d" ]
+            self.config.publicKey;
+        defaultText = literalExpression "publicKey";
+        example = "bernd";
+        type = types.str;
+        description = lib.mdDoc "Name used to derive peer unit name.";
+      };
+
       publicKey = mkOption {
         example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
-        type = types.str;
+        type = types.singleLineStr;
         description = lib.mdDoc "The base64 public key of the peer.";
       };
 
@@ -313,15 +325,11 @@ let
         '';
       };
 
-  peerUnitServiceName = interfaceName: publicKey: dynamicRefreshEnabled:
+  peerUnitServiceName = interfaceName: peerName: dynamicRefreshEnabled:
     let
-      keyToUnitName = replaceStrings
-        [ "/" "-"    " "     "+"     "="      ]
-        [ "-" "\\x2d" "\\x20" "\\x2b" "\\x3d" ];
-      unitName = keyToUnitName publicKey;
       refreshSuffix = optionalString dynamicRefreshEnabled "-refresh";
     in
-      "wireguard-${interfaceName}-peer-${unitName}${refreshSuffix}";
+      "wireguard-${interfaceName}-peer-${peerName}${refreshSuffix}";
 
   generatePeerUnit = { interfaceName, interfaceCfg, peer }:
     let
@@ -337,10 +345,11 @@ let
       # We generate a different name (a `-refresh` suffix) when `dynamicEndpointRefreshSeconds`
       # to avoid that the same service switches `Type` (`oneshot` vs `simple`),
       # with the intent to make scripting more obvious.
-      serviceName = peerUnitServiceName interfaceName peer.publicKey dynamicRefreshEnabled;
+      serviceName = peerUnitServiceName interfaceName peer.name dynamicRefreshEnabled;
     in nameValuePair serviceName
       {
-        description = "WireGuard Peer - ${interfaceName} - ${peer.publicKey}";
+        description = "WireGuard Peer - ${interfaceName} - ${peer.name}"
+          + optionalString (peer.name != peer.publicKey) " (${peer.publicKey})";
         requires = [ "wireguard-${interfaceName}.service" ];
         wants = [ "network-online.target" ];
         after = [ "wireguard-${interfaceName}.service" "network-online.target" ];
@@ -418,7 +427,7 @@ 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";
+      mkPeerUnit = peer: (peerUnitServiceName name peer.name (peer.dynamicEndpointRefreshSeconds != 0)) + ".service";
     in
     nameValuePair "wireguard-${name}"
       rec {
@@ -461,7 +470,7 @@ let
 
           ${ipPreMove} link add dev "${name}" type wireguard
           ${optionalString (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) ''${ipPreMove} link set "${name}" netns "${ns}"''}
-          ${optionalString (values.mtu != null) ''${ipPreMove} link set "${name}" mtu ${toString values.mtu}''}
+          ${optionalString (values.mtu != null) ''${ipPostMove} link set "${name}" mtu ${toString values.mtu}''}
 
           ${concatMapStringsSep "\n" (ip:
             ''${ipPostMove} address add "${ip}" dev "${name}"''
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index 119575bdddb..0595e9e6df2 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -121,11 +121,15 @@ let
         ''}
 
         # substitute environment variables
-        ${pkgs.gawk}/bin/awk '{
-          for(varname in ENVIRON)
-            gsub("@"varname"@", ENVIRON[varname])
-          print
-        }' "${configFile}" > "${finalConfig}"
+        if [ -f "${configFile}" ]; then
+          ${pkgs.gawk}/bin/awk '{
+            for(varname in ENVIRON)
+              gsub("@"varname"@", ENVIRON[varname])
+            print
+          }' "${configFile}" > "${finalConfig}"
+        else
+          touch "${finalConfig}"
+        fi
 
         iface_args="-s ${optionalString cfg.dbusControlled "-u"} -D${cfg.driver} ${configStr}"
 
diff --git a/nixos/modules/services/networking/wstunnel.nix b/nixos/modules/services/networking/wstunnel.nix
new file mode 100644
index 00000000000..067d5df4872
--- /dev/null
+++ b/nixos/modules/services/networking/wstunnel.nix
@@ -0,0 +1,429 @@
+{ config, lib, options, pkgs, utils, ... }:
+with lib;
+let
+  cfg = config.services.wstunnel;
+  attrsToArgs = attrs: utils.escapeSystemdExecArgs (
+    mapAttrsToList
+    (name: value: if value == true then "--${name}" else "--${name}=${value}")
+    attrs
+  );
+  hostPortSubmodule = {
+    options = {
+      host = mkOption {
+        description = mdDoc "The hostname.";
+        type = types.str;
+      };
+      port = mkOption {
+        description = mdDoc "The port.";
+        type = types.port;
+      };
+    };
+  };
+  localRemoteSubmodule = {
+    options = {
+      local = mkOption {
+        description = mdDoc "Local address and port to listen on.";
+        type = types.submodule hostPortSubmodule;
+        example = {
+          host = "127.0.0.1";
+          port = 51820;
+        };
+      };
+      remote = mkOption {
+        description = mdDoc "Address and port on remote to forward traffic to.";
+        type = types.submodule hostPortSubmodule;
+        example = {
+          host = "127.0.0.1";
+          port = 51820;
+        };
+      };
+    };
+  };
+  hostPortToString = { host, port }: "${host}:${builtins.toString port}";
+  localRemoteToString = { local, remote }: utils.escapeSystemdExecArg "${hostPortToString local}:${hostPortToString remote}";
+  commonOptions = {
+    enable = mkOption {
+      description = mdDoc "Whether to enable this `wstunnel` instance.";
+      type = types.bool;
+      default = true;
+    };
+
+    package = mkPackageOptionMD pkgs "wstunnel" {};
+
+    autoStart = mkOption {
+      description = mdDoc "Whether this tunnel server should be started automatically.";
+      type = types.bool;
+      default = true;
+    };
+
+    extraArgs = mkOption {
+      description = mdDoc "Extra command line arguments to pass to `wstunnel`. Attributes of the form `argName = true;` will be translated to `--argName`, and `argName = \"value\"` to `--argName=value`.";
+      type = with types; attrsOf (either str bool);
+      default = {};
+      example = {
+        "someNewOption" = true;
+        "someNewOptionWithValue" = "someValue";
+      };
+    };
+
+    verboseLogging = mkOption {
+      description = mdDoc "Enable verbose logging.";
+      type = types.bool;
+      default = false;
+    };
+
+    environmentFile = mkOption {
+      description = mdDoc "Environment file to be passed to the systemd service. Useful for passing secrets to the service to prevent them from being world-readable in the Nix store. Note however that the secrets are passed to `wstunnel` through the command line, which makes them locally readable for all users of the system at runtime.";
+      type = types.nullOr types.path;
+      default = null;
+      example = "/var/lib/secrets/wstunnelSecrets";
+    };
+  };
+
+  serverSubmodule = { config, ...}: {
+    options = commonOptions // {
+      listen = mkOption {
+        description = mdDoc "Address and port to listen on. Setting the port to a value below 1024 will also give the process the required `CAP_NET_BIND_SERVICE` capability.";
+        type = types.submodule hostPortSubmodule;
+        default = {
+          address = "0.0.0.0";
+          port = if config.enableHTTPS then 443 else 80;
+        };
+        defaultText = literalExpression ''
+          {
+            address = "0.0.0.0";
+            port = if enableHTTPS then 443 else 80;
+          }
+        '';
+      };
+
+      restrictTo = mkOption {
+        description = mdDoc "Accepted traffic will be forwarded only to this service. Set to `null` to allow forwarding to arbitrary addresses.";
+        type = types.nullOr (types.submodule hostPortSubmodule);
+        example = {
+          host = "127.0.0.1";
+          port = 51820;
+        };
+      };
+
+      enableHTTPS = mkOption {
+        description = mdDoc "Use HTTPS for the tunnel server.";
+        type = types.bool;
+        default = true;
+      };
+
+      tlsCertificate = mkOption {
+        description = mdDoc "TLS certificate to use instead of the hardcoded one in case of HTTPS connections. Use together with `tlsKey`.";
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/lib/secrets/cert.pem";
+      };
+
+      tlsKey = mkOption {
+        description = mdDoc "TLS key to use instead of the hardcoded on in case of HTTPS connections. Use together with `tlsCertificate`.";
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/lib/secrets/key.pem";
+      };
+
+      useACMEHost = mkOption {
+        description = mdDoc "Use a certificate generated by the NixOS ACME module for the given host. Note that this will not generate a new certificate - you will need to do so with `security.acme.certs`.";
+        type = types.nullOr types.str;
+        default = null;
+        example = "example.com";
+      };
+    };
+  };
+  clientSubmodule = { config, ... }: {
+    options = commonOptions // {
+      connectTo = mkOption {
+        description = mdDoc "Server address and port to connect to.";
+        type = types.submodule hostPortSubmodule;
+        example = {
+          host = "example.com";
+        };
+      };
+
+      enableHTTPS = mkOption {
+        description = mdDoc "Enable HTTPS when connecting to the server.";
+        type = types.bool;
+        default = true;
+      };
+
+      localToRemote = mkOption {
+        description = mdDoc "Local hosts and ports to listen on, plus the hosts and ports on remote to forward traffic to. Setting a local port to a value less than 1024 will additionally give the process the required CAP_NET_BIND_SERVICE capability.";
+        type = types.listOf (types.submodule localRemoteSubmodule);
+        default = [];
+        example = [ {
+          local = {
+            host = "127.0.0.1";
+            port = 8080;
+          };
+          remote = {
+            host = "127.0.0.1";
+            port = 8080;
+          };
+        } ];
+      };
+
+      dynamicToRemote = mkOption {
+        description = mdDoc "Host and port for the SOCKS5 proxy to dynamically forward traffic to. Leave this at `null` to disable the SOCKS5 proxy. Setting the port to a value less than 1024 will additionally give the service the required CAP_NET_BIND_SERVICE capability.";
+        type = types.nullOr (types.submodule hostPortSubmodule);
+        default = null;
+        example = {
+          host = "127.0.0.1";
+          port = 1080;
+        };
+      };
+
+      udp = mkOption {
+        description = mdDoc "Whether to forward UDP instead of TCP traffic.";
+        type = types.bool;
+        default = false;
+      };
+
+      udpTimeout = mkOption {
+        description = mdDoc "When using UDP forwarding, timeout in seconds after which the tunnel connection is closed. `-1` means no timeout.";
+        type = types.int;
+        default = 30;
+      };
+
+      httpProxy = mkOption {
+        description = mdDoc ''
+          Proxy to use to connect to the wstunnel server (`USER:PASS@HOST:PORT`).
+
+          ::: {.warning}
+          Passwords specified here will be world-readable in the Nix store! To pass a password to the service, point the `environmentFile` option to a file containing `PROXY_PASSWORD=<your-password-here>` and set this option to `<user>:$PROXY_PASSWORD@<host>:<port>`. Note however that this will also locally leak the passwords at runtime via e.g. /proc/<pid>/cmdline.
+
+          :::
+        '';
+        type = types.nullOr types.str;
+        default = null;
+      };
+
+      soMark = mkOption {
+        description = mdDoc "Mark network packets with the SO_MARK sockoption with the specified value. Setting this option will also enable the required `CAP_NET_ADMIN` capability for the systemd service.";
+        type = types.nullOr types.int;
+        default = null;
+      };
+
+      upgradePathPrefix = mkOption {
+        description = mdDoc "Use a specific HTTP path prefix that will show up in the upgrade request to the `wstunnel` server. Useful when running `wstunnel` behind a reverse proxy.";
+        type = types.nullOr types.str;
+        default = null;
+        example = "wstunnel";
+      };
+
+      hostHeader = mkOption {
+        description = mdDoc "Use this as the HTTP host header instead of the real hostname. Useful for circumventing hostname-based firewalls.";
+        type = types.nullOr types.str;
+        default = null;
+      };
+
+      tlsSNI = mkOption {
+        description = mdDoc "Use this as the SNI while connecting via TLS. Useful for circumventing hostname-based firewalls.";
+        type = types.nullOr types.str;
+        default = null;
+      };
+
+      tlsVerifyCertificate = mkOption {
+        description = mdDoc "Whether to verify the TLS certificate of the server. It might be useful to set this to `false` when working with the `tlsSNI` option.";
+        type = types.bool;
+        default = true;
+      };
+
+      # The original argument name `websocketPingFrequency` is a misnomer, as the frequency is the inverse of the interval.
+      websocketPingInterval = mkOption {
+        description = mdDoc "Do a heartbeat ping every N seconds to keep up the websocket connection.";
+        type = types.nullOr types.ints.unsigned;
+        default = null;
+      };
+
+      upgradeCredentials = mkOption {
+        description = mdDoc ''
+          Use these credentials to authenticate during the HTTP upgrade request (Basic authorization type, `USER:[PASS]`).
+
+          ::: {.warning}
+          Passwords specified here will be world-readable in the Nix store! To pass a password to the service, point the `environmentFile` option to a file containing `HTTP_PASSWORD=<your-password-here>` and set this option to `<user>:$HTTP_PASSWORD`. Note however that this will also locally leak the passwords at runtime via e.g. /proc/<pid>/cmdline.
+          :::
+        '';
+        type = types.nullOr types.str;
+        default = null;
+      };
+
+      customHeaders = mkOption {
+        description = mdDoc "Custom HTTP headers to send during the upgrade request.";
+        type = types.attrsOf types.str;
+        default = {};
+        example = {
+          "X-Some-Header" = "some-value";
+        };
+      };
+    };
+  };
+  generateServerUnit = name: serverCfg: {
+    name = "wstunnel-server-${name}";
+    value = {
+      description = "wstunnel server - ${name}";
+      requires = [ "network.target" "network-online.target" ];
+      after = [ "network.target" "network-online.target" ];
+      wantedBy = optional serverCfg.autoStart "multi-user.target";
+
+      serviceConfig = let
+        certConfig = config.security.acme.certs."${serverCfg.useACMEHost}";
+      in {
+        Type = "simple";
+        ExecStart = with serverCfg; let
+          resolvedTlsCertificate = if useACMEHost != null
+            then "${certConfig.directory}/fullchain.pem"
+            else tlsCertificate;
+          resolvedTlsKey = if useACMEHost != null
+            then "${certConfig.directory}/key.pem"
+            else tlsKey;
+        in ''
+          ${package}/bin/wstunnel \
+            --server \
+            ${optionalString (restrictTo != null)     "--restrictTo=${utils.escapeSystemdExecArg (hostPortToString restrictTo)}"} \
+            ${optionalString (resolvedTlsCertificate != null) "--tlsCertificate=${utils.escapeSystemdExecArg resolvedTlsCertificate}"} \
+            ${optionalString (resolvedTlsKey != null)         "--tlsKey=${utils.escapeSystemdExecArg resolvedTlsKey}"} \
+            ${optionalString verboseLogging "--verbose"} \
+            ${attrsToArgs extraArgs} \
+            ${utils.escapeSystemdExecArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString listen}"}
+        '';
+        EnvironmentFile = optional (serverCfg.environmentFile != null) serverCfg.environmentFile;
+        DynamicUser = true;
+        SupplementaryGroups = optional (serverCfg.useACMEHost != null) certConfig.group;
+        PrivateTmp = true;
+        AmbientCapabilities = optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+        NoNewPrivileges = true;
+        RestrictNamespaces = "uts ipc pid user cgroup";
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        PrivateDevices = true;
+        RestrictSUIDSGID = true;
+
+      };
+    };
+  };
+  generateClientUnit = name: clientCfg: {
+    name = "wstunnel-client-${name}";
+    value = {
+      description = "wstunnel client - ${name}";
+      requires = [ "network.target" "network-online.target" ];
+      after = [ "network.target" "network-online.target" ];
+      wantedBy = optional clientCfg.autoStart "multi-user.target";
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = with clientCfg; ''
+          ${package}/bin/wstunnel \
+            ${concatStringsSep " " (builtins.map (x:          "--localToRemote=${localRemoteToString x}") localToRemote)} \
+            ${concatStringsSep " " (mapAttrsToList (n: v:     "--customHeaders=\"${n}: ${v}\"") customHeaders)} \
+            ${optionalString (dynamicToRemote != null)        "--dynamicToRemote=${utils.escapeSystemdExecArg (hostPortToString dynamicToRemote)}"} \
+            ${optionalString udp                              "--udp"} \
+            ${optionalString (httpProxy != null)              "--httpProxy=${httpProxy}"} \
+            ${optionalString (soMark != null)                 "--soMark=${toString soMark}"} \
+            ${optionalString (upgradePathPrefix != null)      "--upgradePathPrefix=${upgradePathPrefix}"} \
+            ${optionalString (hostHeader != null)             "--hostHeader=${hostHeader}"} \
+            ${optionalString (tlsSNI != null)                 "--tlsSNI=${tlsSNI}"} \
+            ${optionalString tlsVerifyCertificate             "--tlsVerifyCertificate"} \
+            ${optionalString (websocketPingInterval != null)  "--websocketPingFrequency=${toString websocketPingInterval}"} \
+            ${optionalString (upgradeCredentials != null)     "--upgradeCredentials=${upgradeCredentials}"} \
+            --udpTimeoutSec=${toString udpTimeout} \
+            ${optionalString verboseLogging "--verbose"} \
+            ${attrsToArgs extraArgs} \
+            ${utils.escapeSystemdExecArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString connectTo}"}
+        '';
+        EnvironmentFile = optional (clientCfg.environmentFile != null) clientCfg.environmentFile;
+        DynamicUser = true;
+        PrivateTmp = true;
+        AmbientCapabilities = (optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]) ++ (optionals ((clientCfg.dynamicToRemote.port or 1024) < 1024 || (any (x: x.local.port < 1024) clientCfg.localToRemote)) [ "CAP_NET_BIND_SERVICE" ]);
+        NoNewPrivileges = true;
+        RestrictNamespaces = "uts ipc pid user cgroup";
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        PrivateDevices = true;
+        RestrictSUIDSGID = true;
+      };
+    };
+  };
+in {
+  options.services.wstunnel = {
+    enable = mkEnableOption (mdDoc "wstunnel");
+
+    servers = mkOption {
+      description = mdDoc "`wstunnel` servers to set up.";
+      type = types.attrsOf (types.submodule serverSubmodule);
+      default = {};
+      example = {
+        "wg-tunnel" = {
+          listen.port = 8080;
+          enableHTTPS = true;
+          tlsCertificate = "/var/lib/secrets/fullchain.pem";
+          tlsKey = "/var/lib/secrets/key.pem";
+          restrictTo = {
+            host = "127.0.0.1";
+            port = 51820;
+          };
+        };
+      };
+    };
+
+    clients = mkOption {
+      description = mdDoc "`wstunnel` clients to set up.";
+      type = types.attrsOf (types.submodule clientSubmodule);
+      default = {};
+      example = {
+        "wg-tunnel" = {
+          connectTo = {
+            host = "example.com";
+            port = 8080;
+          };
+          enableHTTPS = true;
+          localToRemote = {
+            local = {
+              host = "127.0.0.1";
+              port = 51820;
+            };
+            remote = {
+              host = "127.0.0.1";
+              port = 51820;
+            };
+          };
+          udp = true;
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services = (mapAttrs' generateServerUnit (filterAttrs (n: v: v.enable) cfg.servers)) // (mapAttrs' generateClientUnit (filterAttrs (n: v: v.enable) cfg.clients));
+
+    assertions = (mapAttrsToList (name: serverCfg: {
+      assertion = !(serverCfg.useACMEHost != null && (serverCfg.tlsCertificate != null || serverCfg.tlsKey != null));
+      message = ''
+        Options services.wstunnel.servers."${name}".useACMEHost and services.wstunnel.servers."${name}".{tlsCertificate, tlsKey} are mutually exclusive.
+      '';
+    }) cfg.servers) ++
+    (mapAttrsToList (name: serverCfg: {
+      assertion = !((serverCfg.tlsCertificate != null || serverCfg.tlsKey != null) && !(serverCfg.tlsCertificate != null && serverCfg.tlsKey != null));
+      message = ''
+        services.wstunnel.servers."${name}".tlsCertificate and services.wstunnel.servers."${name}".tlsKey need to be set together.
+      '';
+    }) cfg.servers) ++
+    (mapAttrsToList (name: clientCfg: {
+      assertion = !(clientCfg.localToRemote == [] && clientCfg.dynamicToRemote == null);
+      message = ''
+        Either one of services.wstunnel.clients."${name}".localToRemote or services.wstunnel.clients."${name}".dynamicToRemote must be set.
+      '';
+    }) cfg.clients);
+  };
+
+  meta.maintainers = with maintainers; [ alyaeanyx ];
+}
diff --git a/nixos/modules/services/networking/xinetd.nix b/nixos/modules/services/networking/xinetd.nix
index b9120f37ba2..fb3de7077e3 100644
--- a/nixos/modules/services/networking/xinetd.nix
+++ b/nixos/modules/services/networking/xinetd.nix
@@ -27,7 +27,7 @@ let
         ${optionalString srv.unlisted "type        = UNLISTED"}
         ${optionalString (srv.flags != "") "flags = ${srv.flags}"}
         socket_type = ${if srv.protocol == "udp" then "dgram" else "stream"}
-        ${if srv.port != 0 then "port        = ${toString srv.port}" else ""}
+        ${optionalString (srv.port != 0) "port        = ${toString srv.port}"}
         wait        = ${if srv.protocol == "udp" then "yes" else "no"}
         user        = ${srv.user}
         server      = ${srv.server}
diff --git a/nixos/modules/services/networking/xray.nix b/nixos/modules/services/networking/xray.nix
index e2fd83c4dfd..83655a2f88e 100644
--- a/nixos/modules/services/networking/xray.nix
+++ b/nixos/modules/services/networking/xray.nix
@@ -90,6 +90,9 @@ with lib;
       serviceConfig = {
         DynamicUser = true;
         ExecStart = "${cfg.package}/bin/xray -config ${settingsFile}";
+        CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
+        AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
+        NoNewPrivileges = true;
       };
     };
   };
diff --git a/nixos/modules/services/networking/yggdrasil.xml b/nixos/modules/services/networking/yggdrasil.md
index a7b8c469529..bbaea5bc74a 100644
--- a/nixos/modules/services/networking/yggdrasil.xml
+++ b/nixos/modules/services/networking/yggdrasil.md
@@ -1,25 +1,18 @@
-<?xml version="1.0"?>
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude" version="5.0" xml:id="module-services-networking-yggdrasil">
-  <title>Yggdrasil</title>
-  <para>
-    <emphasis>Source:</emphasis>
-    <filename>modules/services/networking/yggdrasil/default.nix</filename>
-  </para>
-  <para>
-    <emphasis>Upstream documentation:</emphasis>
-    <link xlink:href="https://yggdrasil-network.github.io/"/>
-  </para>
-  <para>
+# Yggdrasil {#module-services-networking-yggdrasil}
+
+*Source:* {file}`modules/services/networking/yggdrasil/default.nix`
+
+*Upstream documentation:* <https://yggdrasil-network.github.io/>
+
 Yggdrasil is an early-stage implementation of a fully end-to-end encrypted,
 self-arranging IPv6 network.
-</para>
-  <section xml:id="module-services-networking-yggdrasil-configuration">
-    <title>Configuration</title>
-    <section xml:id="module-services-networking-yggdrasil-configuration-simple">
-      <title>Simple ephemeral node</title>
-      <para>
+
+## Configuration {#module-services-networking-yggdrasil-configuration}
+
+### Simple ephemeral node {#module-services-networking-yggdrasil-configuration-simple}
+
 An annotated example of a simple configuration:
-<programlisting>
+```
 {
   services.yggdrasil = {
     enable = true;
@@ -41,14 +34,12 @@ An annotated example of a simple configuration:
     };
   };
 }
-</programlisting>
-   </para>
-    </section>
-    <section xml:id="module-services-networking-yggdrasil-configuration-prefix">
-      <title>Persistent node with prefix</title>
-      <para>
+```
+
+### Persistent node with prefix {#module-services-networking-yggdrasil-configuration-prefix}
+
 A node with a fixed address that announces a prefix:
-<programlisting>
+```
 let
   address = "210:5217:69c0:9afc:1b95:b9f:8718:c3d2";
   prefix = "310:5217:69c0:9afc";
@@ -93,15 +84,13 @@ in {
     '';
   };
 }
-</programlisting>
-  </para>
-    </section>
-    <section xml:id="module-services-networking-yggdrasil-configuration-container">
-      <title>Yggdrasil attached Container</title>
-      <para>
+```
+
+### Yggdrasil attached Container {#module-services-networking-yggdrasil-configuration-container}
+
 A NixOS container attached to the Yggdrasil network via a node running on the
 host:
-        <programlisting>
+```
 let
   yggPrefix64 = "310:5217:69c0:9afc";
     # Again, taken from the output of "yggdrasilctl getself".
@@ -112,10 +101,10 @@ in
 
   networking = {
     bridges.br0.interfaces = [ ];
-    # A bridge only to containers&#x2026;
+    # A bridge only to containers…
 
     interfaces.br0 = {
-      # &#x2026; configured with a prefix address.
+      # … configured with a prefix address.
       ipv6.addresses = [{
         address = "${yggPrefix64}::1";
         prefixLength = 64;
@@ -149,8 +138,4 @@ in
   };
 
 }
-</programlisting>
-      </para>
-    </section>
-  </section>
-</chapter>
+```
diff --git a/nixos/modules/services/networking/yggdrasil.nix b/nixos/modules/services/networking/yggdrasil.nix
index 3d5cbdd2dc3..55a6002d61a 100644
--- a/nixos/modules/services/networking/yggdrasil.nix
+++ b/nixos/modules/services/networking/yggdrasil.nix
@@ -8,7 +8,8 @@ let
   configFileProvided = cfg.configFile != null;
 
   format = pkgs.formats.json { };
-in {
+in
+{
   imports = [
     (mkRenamedOptionModule
       [ "services" "yggdrasil" "config" ]
@@ -21,7 +22,7 @@ in {
 
       settings = mkOption {
         type = format.type;
-        default = {};
+        default = { };
         example = {
           Peers = [
             "tcp://aa.bb.cc.dd:eeeee"
@@ -45,7 +46,7 @@ in {
 
           If no keys are specified then ephemeral keys are generated
           and the Yggdrasil interface will have a random IPv6 address
-          each time the service is started, this is the default.
+          each time the service is started. This is the default.
 
           If both {option}`configFile` and {option}`settings`
           are supplied, they will be combined, with values from
@@ -61,8 +62,13 @@ in {
         default = null;
         example = "/run/keys/yggdrasil.conf";
         description = lib.mdDoc ''
-          A file which contains JSON configuration for yggdrasil.
-          See the {option}`settings` option for more information.
+          A file which contains JSON or HJSON configuration for yggdrasil. See
+          the {option}`settings` option for more information.
+
+          Note: This file must not be larger than 1 MB because it is passed to
+          the yggdrasil process via systemd‘s LoadCredential mechanism. For
+          details, see <https://systemd.io/CREDENTIALS/> and `man 5
+          systemd.exec`.
         '';
       };
 
@@ -77,20 +83,20 @@ in {
         type = bool;
         default = false;
         description = lib.mdDoc ''
-          Whether to open the UDP port used for multicast peer
-          discovery. The NixOS firewall blocks link-local
-          communication, so in order to make local peering work you
-          will also need to set `LinkLocalTCPPort` in your
-          yggdrasil configuration ({option}`settings` or
-          {option}`configFile`) to a port number other than 0,
-          and then add that port to
-          {option}`networking.firewall.allowedTCPPorts`.
+          Whether to open the UDP port used for multicast peer discovery. The
+          NixOS firewall blocks link-local communication, so in order to make
+          incoming local peering work you will also need to configure
+          `MulticastInterfaces` in your Yggdrasil configuration
+          ({option}`settings` or {option}`configFile`). You will then have to
+          add the ports that you configure there to your firewall configuration
+          ({option}`networking.firewall.allowedTCPPorts` or
+          {option}`networking.firewall.interfaces.<name>.allowedTCPPorts`).
         '';
       };
 
       denyDhcpcdInterfaces = mkOption {
         type = listOf str;
-        default = [];
+        default = [ ];
         example = [ "tap*" ];
         description = lib.mdDoc ''
           Disable the DHCP client for any interface whose name matches
@@ -118,82 +124,104 @@ in {
     };
   };
 
-  config = mkIf cfg.enable (let binYggdrasil = cfg.package + "/bin/yggdrasil";
-  in {
-    assertions = [{
-      assertion = config.networking.enableIPv6;
-      message = "networking.enableIPv6 must be true for yggdrasil to work";
-    }];
-
-    system.activationScripts.yggdrasil = mkIf cfg.persistentKeys ''
-      if [ ! -e ${keysPath} ]
-      then
-        mkdir --mode=700 -p ${builtins.dirOf keysPath}
-        ${binYggdrasil} -genconf -json \
-          | ${pkgs.jq}/bin/jq \
-              'to_entries|map(select(.key|endswith("Key")))|from_entries' \
-          > ${keysPath}
-      fi
-    '';
-
-    systemd.services.yggdrasil = {
-      description = "Yggdrasil Network Service";
-      after = [ "network-pre.target" ];
-      wants = [ "network.target" ];
-      before = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-
-      preStart =
-        (if settingsProvided || configFileProvided || cfg.persistentKeys then
-          "echo "
-
-          + (lib.optionalString settingsProvided
-            "'${builtins.toJSON cfg.settings}'")
-          + (lib.optionalString configFileProvided "$(cat ${cfg.configFile})")
-          + (lib.optionalString cfg.persistentKeys "$(cat ${keysPath})")
-          + " | ${pkgs.jq}/bin/jq -s add | ${binYggdrasil} -normaliseconf -useconf"
-        else
-          "${binYggdrasil} -genconf") + " > /run/yggdrasil/yggdrasil.conf";
-
-      serviceConfig = {
-        ExecStart =
-          "${binYggdrasil} -useconffile /run/yggdrasil/yggdrasil.conf";
-        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-        Restart = "always";
-
-        DynamicUser = true;
-        StateDirectory = "yggdrasil";
-        RuntimeDirectory = "yggdrasil";
-        RuntimeDirectoryMode = "0750";
-        BindReadOnlyPaths = lib.optional configFileProvided cfg.configFile
-          ++ lib.optional cfg.persistentKeys keysPath;
-        ReadWritePaths = "/run/yggdrasil";
-
-        AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
-        CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
-        MemoryDenyWriteExecute = true;
-        ProtectControlGroups = true;
-        ProtectHome = "tmpfs";
-        ProtectKernelModules = true;
-        ProtectKernelTunables = true;
-        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
-        RestrictNamespaces = true;
-        RestrictRealtime = true;
-        SystemCallArchitectures = "native";
-        SystemCallFilter = [ "@system-service" "~@privileged @keyring" ];
-      } // (if (cfg.group != null) then {
-        Group = cfg.group;
-      } else {});
-    };
+  config = mkIf cfg.enable (
+    let
+      binYggdrasil = "${cfg.package}/bin/yggdrasil";
+      binHjson = "${pkgs.hjson-go}/bin/hjson-cli";
+    in
+    {
+      assertions = [{
+        assertion = config.networking.enableIPv6;
+        message = "networking.enableIPv6 must be true for yggdrasil to work";
+      }];
+
+      system.activationScripts.yggdrasil = mkIf cfg.persistentKeys ''
+        if [ ! -e ${keysPath} ]
+        then
+          mkdir --mode=700 -p ${builtins.dirOf keysPath}
+          ${binYggdrasil} -genconf -json \
+            | ${pkgs.jq}/bin/jq \
+                'to_entries|map(select(.key|endswith("Key")))|from_entries' \
+            > ${keysPath}
+        fi
+      '';
+
+      systemd.services.yggdrasil = {
+        description = "Yggdrasil Network Service";
+        after = [ "network-pre.target" ];
+        wants = [ "network.target" ];
+        before = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        # This script first prepares the config file, then it starts Yggdrasil.
+        # The preparation could also be done in ExecStartPre/preStart but only
+        # systemd versions >= v252 support reading credentials in ExecStartPre. As
+        # of February 2023, systemd v252 is not yet in the stable branch of NixOS.
+        #
+        # This could be changed in the future once systemd version v252 has
+        # reached NixOS but it does not have to be. Config file preparation is
+        # fast enough, it does not need elevated privileges, and `set -euo
+        # pipefail` should make sure that the service is not started if the
+        # preparation fails. Therefore, it is not necessary to move the
+        # preparation to ExecStartPre.
+        script = ''
+          set -euo pipefail
+
+          # prepare config file
+          ${(if settingsProvided || configFileProvided || cfg.persistentKeys then
+            "echo "
+
+            + (lib.optionalString settingsProvided
+              "'${builtins.toJSON cfg.settings}'")
+            + (lib.optionalString configFileProvided
+              "$(${binHjson} -c \"$CREDENTIALS_DIRECTORY/yggdrasil.conf\")")
+            + (lib.optionalString cfg.persistentKeys "$(cat ${keysPath})")
+            + " | ${pkgs.jq}/bin/jq -s add | ${binYggdrasil} -normaliseconf -useconf"
+          else
+            "${binYggdrasil} -genconf") + " > /run/yggdrasil/yggdrasil.conf"}
+
+          # start yggdrasil
+          ${binYggdrasil} -useconffile /run/yggdrasil/yggdrasil.conf
+        '';
+
+        serviceConfig = {
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          Restart = "always";
+
+          DynamicUser = true;
+          StateDirectory = "yggdrasil";
+          RuntimeDirectory = "yggdrasil";
+          RuntimeDirectoryMode = "0750";
+          BindReadOnlyPaths = lib.optional cfg.persistentKeys keysPath;
+          LoadCredential =
+            mkIf configFileProvided "yggdrasil.conf:${cfg.configFile}";
+
+          AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
+          CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
+          MemoryDenyWriteExecute = true;
+          ProtectControlGroups = true;
+          ProtectHome = "tmpfs";
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [ "@system-service" "~@privileged @keyring" ];
+        } // (if (cfg.group != null) then {
+          Group = cfg.group;
+        } else { });
+      };
 
-    networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces;
-    networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ];
+      networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces;
+      networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ];
 
-    # Make yggdrasilctl available on the command line.
-    environment.systemPackages = [ cfg.package ];
-  });
+      # Make yggdrasilctl available on the command line.
+      environment.systemPackages = [ cfg.package ];
+    }
+  );
   meta = {
-    doc = ./yggdrasil.xml;
+    doc = ./yggdrasil.md;
     maintainers = with lib.maintainers; [ gazally ehmry ];
   };
 }
diff --git a/nixos/modules/services/networking/zerobin.nix b/nixos/modules/services/networking/zerobin.nix
index 9e07666f3e1..735d4fa25fb 100644
--- a/nixos/modules/services/networking/zerobin.nix
+++ b/nixos/modules/services/networking/zerobin.nix
@@ -75,13 +75,12 @@ in
 
     config = mkIf (cfg.enable) {
       users.users.${cfg.user} =
-      if cfg.user == "zerobin" then {
+      optionalAttrs (cfg.user == "zerobin") {
         isSystemUser = true;
         group = cfg.group;
         home = cfg.dataDir;
         createHome = true;
-      }
-      else {};
+      };
       users.groups.${cfg.group} = {};
 
       systemd.services.zerobin = {
diff --git a/nixos/modules/services/networking/znc/default.nix b/nixos/modules/services/networking/znc/default.nix
index 2befab373ba..d3ba4a52419 100644
--- a/nixos/modules/services/networking/znc/default.nix
+++ b/nixos/modules/services/networking/znc/default.nix
@@ -169,7 +169,7 @@ in
           gracefully be applied to this option.
 
           If you intend to update the configuration through this option, be sure
-          to enable {option}`services.znc.mutable`, otherwise none of the
+          to disable {option}`services.znc.mutable`, otherwise none of the
           changes here will be applied after the initial deploy.
         '';
       };
diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix
index ae59dcc226d..f6a23fb900f 100644
--- a/nixos/modules/services/printing/cupsd.nix
+++ b/nixos/modules/services/printing/cupsd.nix
@@ -317,6 +317,7 @@ in
     environment.etc.cups.source = "/var/lib/cups";
 
     services.dbus.packages = [ cups.out ] ++ optional polkitEnabled cups-pk-helper;
+    services.udev.packages = cfg.drivers;
 
     # Allow asswordless printer admin for members of wheel group
     security.polkit.extraConfig = mkIf polkitEnabled ''
@@ -341,7 +342,7 @@ in
 
     systemd.sockets.cups = mkIf cfg.startWhenNeeded {
       wantedBy = [ "sockets.target" ];
-      listenStreams = [ "/run/cups/cups.sock" ]
+      listenStreams = [ "" "/run/cups/cups.sock" ]
         ++ map (x: replaceStrings ["localhost"] ["127.0.0.1"] (removePrefix "*:" x)) cfg.listenAddresses;
     };
 
@@ -395,10 +396,7 @@ in
             ''}
           '';
 
-          serviceConfig = {
-            PrivateTmp = true;
-            RuntimeDirectory = [ "cups" ];
-          };
+          serviceConfig.PrivateTmp = true;
       };
 
     systemd.services.cups-browsed = mkIf avahiEnabled
diff --git a/nixos/modules/services/search/kibana.nix b/nixos/modules/services/search/kibana.nix
index 5eb2381d5d3..a5e132d5c38 100644
--- a/nixos/modules/services/search/kibana.nix
+++ b/nixos/modules/services/search/kibana.nix
@@ -130,9 +130,9 @@ in {
 
           This defaults to the singleton list [ca] when the {option}`ca` option is defined.
         '';
-        default = if cfg.elasticsearch.ca == null then [] else [ca];
+        default = lib.optional (cfg.elasticsearch.ca != null) ca;
         defaultText = literalExpression ''
-          if config.${opt.elasticsearch.ca} == null then [ ] else [ ca ]
+          lib.optional (config.${opt.elasticsearch.ca} != null) ca
         '';
         type = types.listOf types.path;
       };
diff --git a/nixos/modules/services/search/meilisearch.md b/nixos/modules/services/search/meilisearch.md
index 98e7c542cb9..299f56bf829 100644
--- a/nixos/modules/services/search/meilisearch.md
+++ b/nixos/modules/services/search/meilisearch.md
@@ -2,7 +2,7 @@
 
 Meilisearch is a lightweight, fast and powerful search engine. Think elastic search with a much smaller footprint.
 
-## Quickstart
+## Quickstart {#module-services-meilisearch-quickstart}
 
 the minimum to start meilisearch is
 
@@ -14,26 +14,26 @@ this will start the http server included with meilisearch on port 7700.
 
 test with `curl -X GET 'http://localhost:7700/health'`
 
-## Usage
+## Usage {#module-services-meilisearch-usage}
 
 you first need to add documents to an index before you can search for documents.
 
-### Add a documents to the `movies` index
+### Add a documents to the `movies` index {#module-services-meilisearch-quickstart-add}
 
 `curl -X POST 'http://127.0.0.1:7700/indexes/movies/documents' --data '[{"id": "123", "title": "Superman"}, {"id": 234, "title": "Batman"}]'`
 
-### Search documents in the `movies` index
+### Search documents in the `movies` index {#module-services-meilisearch-quickstart-search}
 
 `curl 'http://127.0.0.1:7700/indexes/movies/search' --data '{ "q": "botman" }'` (note the typo is intentional and there to demonstrate the typo tolerant capabilities)
 
-## Defaults
+## Defaults {#module-services-meilisearch-defaults}
 
 - The default nixos package doesn't come with the [dashboard](https://docs.meilisearch.com/learn/getting_started/quick_start.html#search), since the dashboard features makes some assets downloads at compile time.
 
-- Anonimized Analytics sent to meilisearch are disabled by default.
+- Anonymized Analytics sent to meilisearch are disabled by default.
 
 - Default deployment is development mode. It doesn't require a secret master key. All routes are not protected and accessible.
 
-## Missing
+## Missing {#module-services-meilisearch-missing}
 
 - the snapshot feature is not yet configurable from the module, it's just a matter of adding the relevant environment variables.
diff --git a/nixos/modules/services/search/meilisearch.nix b/nixos/modules/services/search/meilisearch.nix
index 3983b1b2c92..7c9fa62ae95 100644
--- a/nixos/modules/services/search/meilisearch.nix
+++ b/nixos/modules/services/search/meilisearch.nix
@@ -9,9 +9,7 @@ in
 {
 
   meta.maintainers = with maintainers; [ Br1ght0ne happysalada ];
-  # Don't edit the docbook xml directly, edit the md and generate it:
-  # `pandoc meilisearch.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > meilisearch.xml`
-  meta.doc = ./meilisearch.xml;
+  meta.doc = ./meilisearch.md;
 
   ###### interface
 
@@ -117,7 +115,7 @@ in
         MEILI_HTTP_ADDR = "${cfg.listenAddress}:${toString cfg.listenPort}";
         MEILI_NO_ANALYTICS = toString cfg.noAnalytics;
         MEILI_ENV = cfg.environment;
-        MEILI_DUMPS_DIR = "/var/lib/meilisearch/dumps";
+        MEILI_DUMP_DIR = "/var/lib/meilisearch/dumps";
         MEILI_LOG_LEVEL = cfg.logLevel;
         MEILI_MAX_INDEX_SIZE = cfg.maxIndexSize;
       };
diff --git a/nixos/modules/services/search/meilisearch.xml b/nixos/modules/services/search/meilisearch.xml
deleted file mode 100644
index c1a73f358c2..00000000000
--- a/nixos/modules/services/search/meilisearch.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-meilisearch">
-  <title>Meilisearch</title>
-  <para>
-    Meilisearch is a lightweight, fast and powerful search engine. Think
-    elastic search with a much smaller footprint.
-  </para>
-  <section xml:id="quickstart">
-    <title>Quickstart</title>
-    <para>
-      the minimum to start meilisearch is
-    </para>
-    <programlisting language="bash">
-services.meilisearch.enable = true;
-</programlisting>
-    <para>
-      this will start the http server included with meilisearch on port
-      7700.
-    </para>
-    <para>
-      test with
-      <literal>curl -X GET 'http://localhost:7700/health'</literal>
-    </para>
-  </section>
-  <section xml:id="usage">
-    <title>Usage</title>
-    <para>
-      you first need to add documents to an index before you can search
-      for documents.
-    </para>
-    <section xml:id="add-a-documents-to-the-movies-index">
-      <title>Add a documents to the <literal>movies</literal>
-      index</title>
-      <para>
-        <literal>curl -X POST 'http://127.0.0.1:7700/indexes/movies/documents' --data '[{&quot;id&quot;: &quot;123&quot;, &quot;title&quot;: &quot;Superman&quot;}, {&quot;id&quot;: 234, &quot;title&quot;: &quot;Batman&quot;}]'</literal>
-      </para>
-    </section>
-    <section xml:id="search-documents-in-the-movies-index">
-      <title>Search documents in the <literal>movies</literal>
-      index</title>
-      <para>
-        <literal>curl 'http://127.0.0.1:7700/indexes/movies/search' --data '{ &quot;q&quot;: &quot;botman&quot; }'</literal>
-        (note the typo is intentional and there to demonstrate the typo
-        tolerant capabilities)
-      </para>
-    </section>
-  </section>
-  <section xml:id="defaults">
-    <title>Defaults</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The default nixos package doesn’t come with the
-          <link xlink:href="https://docs.meilisearch.com/learn/getting_started/quick_start.html#search">dashboard</link>,
-          since the dashboard features makes some assets downloads at
-          compile time.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Anonimized Analytics sent to meilisearch are disabled by
-          default.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Default deployment is development mode. It doesn’t require a
-          secret master key. All routes are not protected and
-          accessible.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="missing">
-    <title>Missing</title>
-    <itemizedlist spacing="compact">
-      <listitem>
-        <para>
-          the snapshot feature is not yet configurable from the module,
-          it’s just a matter of adding the relevant environment
-          variables.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/search/opensearch.nix b/nixos/modules/services/search/opensearch.nix
new file mode 100644
index 00000000000..9a50e796313
--- /dev/null
+++ b/nixos/modules/services/search/opensearch.nix
@@ -0,0 +1,248 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.opensearch;
+
+  settingsFormat = pkgs.formats.yaml {};
+
+  configDir = cfg.dataDir + "/config";
+
+  usingDefaultDataDir = cfg.dataDir == "/var/lib/opensearch";
+  usingDefaultUserAndGroup = cfg.user == "opensearch" && cfg.group == "opensearch";
+
+  opensearchYml = settingsFormat.generate "opensearch.yml" cfg.settings;
+
+  loggingConfigFilename = "log4j2.properties";
+  loggingConfigFile = pkgs.writeTextFile {
+    name = loggingConfigFilename;
+    text = cfg.logging;
+  };
+in
+{
+
+  options.services.opensearch = {
+    enable = mkEnableOption (lib.mdDoc "OpenSearch");
+
+    package = lib.mkPackageOptionMD pkgs "OpenSearch" {
+      default = [ "opensearch" ];
+    };
+
+    settings = lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+
+        options."network.host" = lib.mkOption {
+          type = lib.types.str;
+          default = "127.0.0.1";
+          description = lib.mdDoc ''
+            Which port this service should listen on.
+          '';
+        };
+
+        options."cluster.name" = lib.mkOption {
+          type = lib.types.str;
+          default = "opensearch";
+          description = lib.mdDoc ''
+            The name of the cluster.
+          '';
+        };
+
+        options."discovery.type" = lib.mkOption {
+          type = lib.types.str;
+          default = "single-node";
+          description = lib.mdDoc ''
+            The type of discovery to use.
+          '';
+        };
+
+        options."http.port" = lib.mkOption {
+          type = lib.types.port;
+          default = 9200;
+          description = lib.mdDoc ''
+            The port to listen on for HTTP traffic.
+          '';
+        };
+
+        options."transport.port" = lib.mkOption {
+          type = lib.types.port;
+          default = 9300;
+          description = lib.mdDoc ''
+            The port to listen on for transport traffic.
+          '';
+        };
+      };
+
+      default = {};
+
+      description = lib.mdDoc ''
+        OpenSearch configuration.
+      '';
+    };
+
+    logging = lib.mkOption {
+      description = lib.mdDoc "opensearch logging configuration.";
+
+      default = ''
+        logger.action.name = org.opensearch.action
+        logger.action.level = info
+
+        appender.console.type = Console
+        appender.console.name = console
+        appender.console.layout.type = PatternLayout
+        appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n
+
+        rootLogger.level = info
+        rootLogger.appenderRef.console.ref = console
+      '';
+      type = types.str;
+    };
+
+    dataDir = lib.mkOption {
+      type = lib.types.path;
+      default = "/var/lib/opensearch";
+      apply = converge (removeSuffix "/");
+      description = lib.mdDoc ''
+        Data directory for OpenSearch. If you change this, you need to
+        manually create the directory. You also need to create the
+        `opensearch` user and group, or change
+        [](#opt-services.opensearch.user) and
+        [](#opt-services.opensearch.group) to existing ones with
+        access to the directory.
+      '';
+    };
+
+    user = lib.mkOption {
+      type = lib.types.str;
+      default = "opensearch";
+      description = lib.mdDoc ''
+        The user OpenSearch runs as. Should be left at default unless
+        you have very specific needs.
+      '';
+    };
+
+    group = lib.mkOption {
+      type = lib.types.str;
+      default = "opensearch";
+      description = lib.mdDoc ''
+        The group OpenSearch runs as. Should be left at default unless
+        you have very specific needs.
+      '';
+    };
+
+    extraCmdLineOptions = lib.mkOption {
+      description = lib.mdDoc "Extra command line options for the OpenSearch launcher.";
+      default = [ ];
+      type = lib.types.listOf lib.types.str;
+    };
+
+    extraJavaOptions = lib.mkOption {
+      description = lib.mdDoc "Extra command line options for Java.";
+      default = [ ];
+      type = lib.types.listOf lib.types.str;
+      example = [ "-Djava.net.preferIPv4Stack=true" ];
+    };
+
+    restartIfChanged = lib.mkOption {
+      type = lib.types.bool;
+      description = lib.mdDoc ''
+        Automatically restart the service on config change.
+        This can be set to false to defer restarts on a server or cluster.
+        Please consider the security implications of inadvertently running an older version,
+        and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option.
+      '';
+      default = true;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.opensearch = {
+      description = "OpenSearch Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = [ pkgs.inetutils ];
+      inherit (cfg) restartIfChanged;
+      environment = {
+        OPENSEARCH_HOME = cfg.dataDir;
+        OPENSEARCH_JAVA_OPTS = toString cfg.extraJavaOptions;
+        OPENSEARCH_PATH_CONF = configDir;
+      };
+      serviceConfig = {
+        ExecStartPre =
+          let
+            startPreFullPrivileges = ''
+              set -o errexit -o pipefail -o nounset -o errtrace
+              shopt -s inherit_errexit
+            '' + (optionalString (!config.boot.isContainer) ''
+              # Only set vm.max_map_count if lower than ES required minimum
+              # This avoids conflict if configured via boot.kernel.sysctl
+              if [ $(${pkgs.procps}/bin/sysctl -n vm.max_map_count) -lt 262144 ]; then
+                ${pkgs.procps}/bin/sysctl -w vm.max_map_count=262144
+              fi
+            '');
+            startPreUnprivileged = ''
+              set -o errexit -o pipefail -o nounset -o errtrace
+              shopt -s inherit_errexit
+
+              # Install plugins
+              ln -sfT ${cfg.package}/lib ${cfg.dataDir}/lib
+              ln -sfT ${cfg.package}/modules ${cfg.dataDir}/modules
+
+              # opensearch needs to create the opensearch.keystore in the config directory
+              # so this directory needs to be writable.
+              mkdir -p ${configDir}
+              chmod 0700 ${configDir}
+
+              # Note that we copy config files from the nix store instead of symbolically linking them
+              # because otherwise X-Pack Security will raise the following exception:
+              # java.security.AccessControlException:
+              # access denied ("java.io.FilePermission" "/var/lib/opensearch/config/opensearch.yml" "read")
+
+              rm -f ${configDir}/opensearch.yml
+              cp ${opensearchYml} ${configDir}/opensearch.yml
+
+              # Make sure the logging configuration for old OpenSearch versions is removed:
+              rm -f "${configDir}/logging.yml"
+              rm -f ${configDir}/${loggingConfigFilename}
+              cp ${loggingConfigFile} ${configDir}/${loggingConfigFilename}
+              mkdir -p ${configDir}/scripts
+
+              rm -f ${configDir}/jvm.options
+              cp ${cfg.package}/config/jvm.options ${configDir}/jvm.options
+
+              # redirect jvm logs to the data directory
+              mkdir -p ${cfg.dataDir}/logs
+              chmod 0700 ${cfg.dataDir}/logs
+              sed -e '#logs/gc.log#${cfg.dataDir}/logs/gc.log#' -i ${configDir}/jvm.options
+            '';
+          in [
+            "+${pkgs.writeShellScript "opensearch-start-pre-full-privileges" startPreFullPrivileges}"
+            "${pkgs.writeShellScript "opensearch-start-pre-unprivileged" startPreUnprivileged}"
+          ];
+        ExecStartPost = pkgs.writeShellScript "opensearch-start-post" ''
+          set -o errexit -o pipefail -o nounset -o errtrace
+          shopt -s inherit_errexit
+
+          # Make sure opensearch is up and running before dependents
+          # are started
+          while ! ${pkgs.curl}/bin/curl -sS -f http://${cfg.settings."network.host"}:${toString cfg.settings."http.port"} 2>/dev/null; do
+            sleep 1
+          done
+        '';
+        ExecStart = "${cfg.package}/bin/opensearch ${toString cfg.extraCmdLineOptions}";
+        User = cfg.user;
+        Group = cfg.group;
+        LimitNOFILE = "1024000";
+        Restart = "always";
+        TimeoutStartSec = "infinity";
+        DynamicUser = usingDefaultUserAndGroup && usingDefaultDataDir;
+      } // (optionalAttrs (usingDefaultDataDir) {
+        StateDirectory = "opensearch";
+        StateDirectoryMode = "0700";
+      });
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/search/qdrant.nix b/nixos/modules/services/search/qdrant.nix
new file mode 100644
index 00000000000..e1f7365d951
--- /dev/null
+++ b/nixos/modules/services/search/qdrant.nix
@@ -0,0 +1,129 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+
+  cfg = config.services.qdrant;
+
+  settingsFormat = pkgs.formats.yaml { };
+  configFile = settingsFormat.generate "config.yaml" cfg.settings;
+in {
+
+  options = {
+    services.qdrant = {
+      enable = mkEnableOption (lib.mdDoc "Vector Search Engine for the next generation of AI applications");
+
+      settings = mkOption {
+        description = lib.mdDoc ''
+          Configuration for Qdrant
+          Refer to <https://github.com/qdrant/qdrant/blob/master/config/config.yaml> for details on supported values.
+        '';
+
+        type = settingsFormat.type;
+
+        example = {
+          storage = {
+            storage_path = "/var/lib/qdrant/storage";
+            snapshots_path = "/var/lib/qdrant/snapshots";
+          };
+          hsnw_index = {
+            on_disk = true;
+          };
+          service = {
+            host = "127.0.0.1";
+            http_port = 6333;
+            grpc_port = 6334;
+          };
+          telemetry_disabled = true;
+        };
+
+        defaultText = literalExpression ''
+          {
+            storage = {
+              storage_path = "/var/lib/qdrant/storage";
+              snapshots_path = "/var/lib/qdrant/snapshots";
+            };
+            hsnw_index = {
+              on_disk = true;
+            };
+            service = {
+              host = "127.0.0.1";
+              http_port = 6333;
+              grpc_port = 6334;
+            };
+            telemetry_disabled = true;
+          }
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.qdrant.settings = {
+      storage.storage_path = mkDefault "/var/lib/qdrant/storage";
+      storage.snapshots_path = mkDefault "/var/lib/qdrant/snapshots";
+      # The following default values are the same as in the default config,
+      # they are just written here for convenience.
+      storage.on_disk_payload = mkDefault true;
+      storage.wal.wal_capacity_mb = mkDefault 32;
+      storage.wal.wal_segments_ahead = mkDefault 0;
+      storage.performance.max_search_threads = mkDefault 0;
+      storage.performance.max_optimization_threads = mkDefault 1;
+      storage.optimizers.deleted_threshold = mkDefault 0.2;
+      storage.optimizers.vacuum_min_vector_number = mkDefault 1000;
+      storage.optimizers.default_segment_number = mkDefault 0;
+      storage.optimizers.max_segment_size_kb = mkDefault null;
+      storage.optimizers.memmap_threshold_kb = mkDefault null;
+      storage.optimizers.indexing_threshold_kb = mkDefault 20000;
+      storage.optimizers.flush_interval_sec = mkDefault 5;
+      storage.optimizers.max_optimization_threads = mkDefault 1;
+      storage.hnsw_index.m = mkDefault 16;
+      storage.hnsw_index.ef_construct = mkDefault 100;
+      storage.hnsw_index.full_scan_threshold_kb = mkDefault 10000;
+      storage.hnsw_index.max_indexing_threads = mkDefault 0;
+      storage.hnsw_index.on_disk = mkDefault false;
+      storage.hnsw_index.payload_m = mkDefault null;
+      service.max_request_size_mb = mkDefault 32;
+      service.max_workers = mkDefault 0;
+      service.http_port = mkDefault 6333;
+      service.grpc_port = mkDefault 6334;
+      service.enable_cors = mkDefault true;
+      cluster.enabled = mkDefault false;
+      # the following have been altered for security
+      service.host = mkDefault "127.0.0.1";
+      telemetry_disabled = mkDefault true;
+    };
+
+    systemd.services.qdrant = {
+      description = "Vector Search Engine for the next generation of AI applications";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        LimitNOFILE=65536;
+        ExecStart = "${pkgs.qdrant}/bin/qdrant --config-path ${configFile}";
+        DynamicUser = true;
+        Restart = "on-failure";
+        StateDirectory = "qdrant";
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectClock = true;
+        ProtectProc = "noaccess";
+        ProcSubset = "pid";
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        RestrictSUIDSGID = true;
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RemoveIPC = true;
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/search/solr.nix b/nixos/modules/services/search/solr.nix
deleted file mode 100644
index 05592e9fa24..00000000000
--- a/nixos/modules/services/search/solr.nix
+++ /dev/null
@@ -1,110 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.solr;
-
-in
-
-{
-  options = {
-    services.solr = {
-      enable = mkEnableOption (lib.mdDoc "Solr");
-
-      package = mkOption {
-        type = types.package;
-        default = pkgs.solr;
-        defaultText = literalExpression "pkgs.solr";
-        description = lib.mdDoc "Which Solr package to use.";
-      };
-
-      port = mkOption {
-        type = types.port;
-        default = 8983;
-        description = lib.mdDoc "Port on which Solr is ran.";
-      };
-
-      stateDir = mkOption {
-        type = types.path;
-        default = "/var/lib/solr";
-        description = lib.mdDoc "The solr home directory containing config, data, and logging files.";
-      };
-
-      extraJavaOptions = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        description = lib.mdDoc "Extra command line options given to the java process running Solr.";
-      };
-
-      user = mkOption {
-        type = types.str;
-        default = "solr";
-        description = lib.mdDoc "User under which Solr is ran.";
-      };
-
-      group = mkOption {
-        type = types.str;
-        default = "solr";
-        description = lib.mdDoc "Group under which Solr is ran.";
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-
-    environment.systemPackages = [ cfg.package ];
-
-    systemd.services.solr = {
-      after = [ "network.target" "remote-fs.target" "nss-lookup.target" "systemd-journald-dev-log.socket" ];
-      wantedBy = [ "multi-user.target" ];
-
-      environment = {
-        SOLR_HOME = "${cfg.stateDir}/data";
-        LOG4J_PROPS = "${cfg.stateDir}/log4j2.xml";
-        SOLR_LOGS_DIR = "${cfg.stateDir}/logs";
-        SOLR_PORT = "${toString cfg.port}";
-      };
-      path = with pkgs; [
-        gawk
-        procps
-      ];
-      preStart = ''
-        mkdir -p "${cfg.stateDir}/data";
-        mkdir -p "${cfg.stateDir}/logs";
-
-        if ! test -e "${cfg.stateDir}/data/solr.xml"; then
-          install -D -m0640 ${cfg.package}/server/solr/solr.xml "${cfg.stateDir}/data/solr.xml"
-          install -D -m0640 ${cfg.package}/server/solr/zoo.cfg "${cfg.stateDir}/data/zoo.cfg"
-        fi
-
-        if ! test -e "${cfg.stateDir}/log4j2.xml"; then
-          install -D -m0640 ${cfg.package}/server/resources/log4j2.xml "${cfg.stateDir}/log4j2.xml"
-        fi
-      '';
-
-      serviceConfig = {
-        User = cfg.user;
-        Group = cfg.group;
-        ExecStart="${cfg.package}/bin/solr start -f -a \"${concatStringsSep " " cfg.extraJavaOptions}\"";
-        ExecStop="${cfg.package}/bin/solr stop";
-      };
-    };
-
-    users.users = optionalAttrs (cfg.user == "solr") {
-      solr = {
-        group = cfg.group;
-        home = cfg.stateDir;
-        createHome = true;
-        uid = config.ids.uids.solr;
-      };
-    };
-
-    users.groups = optionalAttrs (cfg.group == "solr") {
-      solr.gid = config.ids.gids.solr;
-    };
-
-  };
-
-}
diff --git a/nixos/modules/services/security/authelia.nix b/nixos/modules/services/security/authelia.nix
new file mode 100644
index 00000000000..cc55260e20f
--- /dev/null
+++ b/nixos/modules/services/security/authelia.nix
@@ -0,0 +1,401 @@
+{ lib
+, pkgs
+, config
+, ...
+}:
+
+let
+  cfg = config.services.authelia;
+
+  format = pkgs.formats.yaml { };
+  configFile = format.generate "config.yml" cfg.settings;
+
+  autheliaOpts = with lib; { name, ... }: {
+    options = {
+      enable = mkEnableOption (mdDoc "Authelia instance");
+
+      name = mkOption {
+        type = types.str;
+        default = name;
+        description = mdDoc ''
+          Name is used as a suffix for the service name, user, and group.
+          By default it takes the value you use for `<instance>` in:
+          {option}`services.authelia.<instance>`
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.authelia;
+        type = types.package;
+        defaultText = literalExpression "pkgs.authelia";
+        description = mdDoc "Authelia derivation to use.";
+      };
+
+      user = mkOption {
+        default = "authelia-${name}";
+        type = types.str;
+        description = mdDoc "The name of the user for this authelia instance.";
+      };
+
+      group = mkOption {
+        default = "authelia-${name}";
+        type = types.str;
+        description = mdDoc "The name of the group for this authelia instance.";
+      };
+
+      secrets = mkOption {
+        description = mdDoc ''
+          It is recommended you keep your secrets separate from the configuration.
+          It's especially important to keep the raw secrets out of your nix configuration,
+          as the values will be preserved in your nix store.
+          This attribute allows you to configure the location of secret files to be loaded at runtime.
+
+          https://www.authelia.com/configuration/methods/secrets/
+        '';
+        default = { };
+        type = types.submodule {
+          options = {
+            manual = mkOption {
+              default = false;
+              example = true;
+              description = mdDoc ''
+                Configuring authelia's secret files via the secrets attribute set
+                is intended to be convenient and help catch cases where values are required
+                to run at all.
+                If a user wants to set these values themselves and bypass the validation they can set this value to true.
+              '';
+              type = types.bool;
+            };
+
+            # required
+            jwtSecretFile = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = mdDoc ''
+                Path to your JWT secret used during identity verificaton.
+              '';
+            };
+
+            oidcIssuerPrivateKeyFile = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = mdDoc ''
+                Path to your private key file used to encrypt OIDC JWTs.
+              '';
+            };
+
+            oidcHmacSecretFile = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = mdDoc ''
+                Path to your HMAC secret used to sign OIDC JWTs.
+              '';
+            };
+
+            sessionSecretFile = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = mdDoc ''
+                Path to your session secret. Only used when redis is used as session storage.
+              '';
+            };
+
+            # required
+            storageEncryptionKeyFile = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = mdDoc ''
+                Path to your storage encryption key.
+              '';
+            };
+          };
+        };
+      };
+
+      environmentVariables = mkOption {
+        type = types.attrsOf types.str;
+        description = mdDoc ''
+          Additional environment variables to provide to authelia.
+          If you are providing secrets please consider the options under {option}`services.authelia.<instance>.secrets`
+          or make sure you use the `_FILE` suffix.
+          If you provide the raw secret rather than the location of a secret file that secret will be preserved in the nix store.
+          For more details: https://www.authelia.com/configuration/methods/secrets/
+        '';
+        default = { };
+      };
+
+      settings = mkOption {
+        description = mdDoc ''
+          Your Authelia config.yml as a Nix attribute set.
+          There are several values that are defined and documented in nix such as `default_2fa_method`,
+          but additional items can also be included.
+
+          https://github.com/authelia/authelia/blob/master/config.template.yml
+        '';
+        default = { };
+        example = ''
+          {
+            theme = "light";
+            default_2fa_method = "totp";
+            log.level = "debug";
+            server.disable_healthcheck = true;
+          }
+        '';
+        type = types.submodule {
+          freeformType = format.type;
+          options = {
+            theme = mkOption {
+              type = types.enum [ "light" "dark" "grey" "auto" ];
+              default = "light";
+              example = "dark";
+              description = mdDoc "The theme to display.";
+            };
+
+            default_2fa_method = mkOption {
+              type = types.enum [ "" "totp" "webauthn" "mobile_push" ];
+              default = "";
+              example = "webauthn";
+              description = mdDoc ''
+                Default 2FA method for new users and fallback for preferred but disabled methods.
+              '';
+            };
+
+            server = {
+              host = mkOption {
+                type = types.str;
+                default = "localhost";
+                example = "0.0.0.0";
+                description = mdDoc "The address to listen on.";
+              };
+
+              port = mkOption {
+                type = types.port;
+                default = 9091;
+                description = mdDoc "The port to listen on.";
+              };
+            };
+
+            log = {
+              level = mkOption {
+                type = types.enum [ "info" "debug" "trace" ];
+                default = "debug";
+                example = "info";
+                description = mdDoc "Level of verbosity for logs: info, debug, trace.";
+              };
+
+              format = mkOption {
+                type = types.enum [ "json" "text" ];
+                default = "json";
+                example = "text";
+                description = mdDoc "Format the logs are written as.";
+              };
+
+              file_path = mkOption {
+                type = types.nullOr types.path;
+                default = null;
+                example = "/var/log/authelia/authelia.log";
+                description = mdDoc "File path where the logs will be written. If not set logs are written to stdout.";
+              };
+
+              keep_stdout = mkOption {
+                type = types.bool;
+                default = false;
+                example = true;
+                description = mdDoc "Whether to also log to stdout when a `file_path` is defined.";
+              };
+            };
+
+            telemetry = {
+              metrics = {
+                enabled = mkOption {
+                  type = types.bool;
+                  default = false;
+                  example = true;
+                  description = mdDoc "Enable Metrics.";
+                };
+
+                address = mkOption {
+                  type = types.str;
+                  default = "tcp://127.0.0.1:9959";
+                  example = "tcp://0.0.0.0:8888";
+                  description = mdDoc "The address to listen on for metrics. This should be on a different port to the main `server.port` value.";
+                };
+              };
+            };
+          };
+        };
+      };
+
+      settingsFiles = mkOption {
+        type = types.listOf types.path;
+        default = [ ];
+        example = [ "/etc/authelia/config.yml" "/etc/authelia/access-control.yml" "/etc/authelia/config/" ];
+        description = mdDoc ''
+          Here you can provide authelia with configuration files or directories.
+          It is possible to give authelia multiple files and use the nix generated configuration
+          file set via {option}`services.authelia.<instance>.settings`.
+        '';
+      };
+    };
+  };
+in
+{
+  options.services.authelia.instances = with lib; mkOption {
+    default = { };
+    type = types.attrsOf (types.submodule autheliaOpts);
+    description = mdDoc ''
+      Multi-domain protection currently requires multiple instances of Authelia.
+      If you don't require multiple instances of Authelia you can define just the one.
+
+      https://www.authelia.com/roadmap/active/multi-domain-protection/
+    '';
+    example = ''
+      {
+        main = {
+          enable = true;
+          secrets.storageEncryptionKeyFile = "/etc/authelia/storageEncryptionKeyFile";
+          secrets.jwtSecretFile = "/etc/authelia/jwtSecretFile";
+          settings = {
+            theme = "light";
+            default_2fa_method = "totp";
+            log.level = "debug";
+            server.disable_healthcheck = true;
+          };
+        };
+        preprod = {
+          enable = false;
+          secrets.storageEncryptionKeyFile = "/mnt/pre-prod/authelia/storageEncryptionKeyFile";
+          secrets.jwtSecretFile = "/mnt/pre-prod/jwtSecretFile";
+          settings = {
+            theme = "dark";
+            default_2fa_method = "webauthn";
+            server.host = "0.0.0.0";
+          };
+        };
+        test.enable = true;
+        test.secrets.manual = true;
+        test.settings.theme = "grey";
+        test.settings.server.disable_healthcheck = true;
+        test.settingsFiles = [ "/mnt/test/authelia" "/mnt/test-authelia.conf" ];
+        };
+      }
+    '';
+  };
+
+  config =
+    let
+      mkInstanceServiceConfig = instance:
+        let
+          execCommand = "${instance.package}/bin/authelia";
+          configFile = format.generate "config.yml" instance.settings;
+          configArg = "--config ${builtins.concatStringsSep "," (lib.concatLists [[configFile] instance.settingsFiles])}";
+        in
+        {
+          description = "Authelia authentication and authorization server";
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" ];
+          environment =
+            (lib.filterAttrs (_: v: v != null) {
+              AUTHELIA_JWT_SECRET_FILE = instance.secrets.jwtSecretFile;
+              AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = instance.secrets.storageEncryptionKeyFile;
+              AUTHELIA_SESSION_SECRET_FILE = instance.secrets.sessionSecretFile;
+              AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = instance.secrets.oidcIssuerPrivateKeyFile;
+              AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = instance.secrets.oidcHmacSecretFile;
+            })
+            // instance.environmentVariables;
+
+          preStart = "${execCommand} ${configArg} validate-config";
+          serviceConfig = {
+            User = instance.user;
+            Group = instance.group;
+            ExecStart = "${execCommand} ${configArg}";
+            Restart = "always";
+            RestartSec = "5s";
+            StateDirectory = "authelia-${instance.name}";
+            StateDirectoryMode = "0700";
+
+            # Security options:
+            AmbientCapabilities = "";
+            CapabilityBoundingSet = "";
+            DeviceAllow = "";
+            LockPersonality = true;
+            MemoryDenyWriteExecute = true;
+            NoNewPrivileges = true;
+
+            PrivateTmp = true;
+            PrivateDevices = true;
+            PrivateUsers = true;
+
+            ProtectClock = true;
+            ProtectControlGroups = true;
+            ProtectHome = "read-only";
+            ProtectHostname = true;
+            ProtectKernelLogs = true;
+            ProtectKernelModules = true;
+            ProtectKernelTunables = true;
+            ProtectProc = "noaccess";
+            ProtectSystem = "strict";
+
+            RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+            RestrictNamespaces = true;
+            RestrictRealtime = true;
+            RestrictSUIDSGID = true;
+
+            SystemCallArchitectures = "native";
+            SystemCallErrorNumber = "EPERM";
+            SystemCallFilter = [
+              "@system-service"
+              "~@cpu-emulation"
+              "~@debug"
+              "~@keyring"
+              "~@memlock"
+              "~@obsolete"
+              "~@privileged"
+              "~@setuid"
+            ];
+          };
+        };
+      mkInstanceUsersConfig = instance: {
+        groups."authelia-${instance.name}" =
+          lib.mkIf (instance.group == "authelia-${instance.name}") {
+            name = "authelia-${instance.name}";
+          };
+        users."authelia-${instance.name}" =
+          lib.mkIf (instance.user == "authelia-${instance.name}") {
+            name = "authelia-${instance.name}";
+            isSystemUser = true;
+            group = instance.group;
+          };
+      };
+      instances = lib.attrValues cfg.instances;
+    in
+    {
+      assertions = lib.flatten (lib.flip lib.mapAttrsToList cfg.instances (name: instance:
+        [
+          {
+            assertion = instance.secrets.manual || (instance.secrets.jwtSecretFile != null && instance.secrets.storageEncryptionKeyFile != null);
+            message = ''
+              Authelia requires a JWT Secret and a Storage Encryption Key to work.
+              Either set them like so:
+              services.authelia.${name}.secrets.jwtSecretFile = /my/path/to/jwtsecret;
+              services.authelia.${name}.secrets.storageEncryptionKeyFile = /my/path/to/encryptionkey;
+              Or set services.authelia.${name}.secrets.manual = true and provide them yourself via
+              environmentVariables or settingsFiles.
+              Do not include raw secrets in nix settings.
+            '';
+          }
+        ]
+      ));
+
+      systemd.services = lib.mkMerge
+        (map
+          (instance: lib.mkIf instance.enable {
+            "authelia-${instance.name}" = mkInstanceServiceConfig instance;
+          })
+          instances);
+      users = lib.mkMerge
+        (map
+          (instance: lib.mkIf instance.enable (mkInstanceUsersConfig instance))
+          instances);
+    };
+}
diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix
index 3b124a4f0e0..9393fa75128 100644
--- a/nixos/modules/services/security/fail2ban.nix
+++ b/nixos/modules/services/security/fail2ban.nix
@@ -3,23 +3,44 @@
 with lib;
 
 let
-
   cfg = config.services.fail2ban;
 
-  fail2banConf = pkgs.writeText "fail2ban.local" cfg.daemonConfig;
+  settingsFormat = pkgs.formats.keyValue { };
 
-  jailConf = pkgs.writeText "jail.local" ''
-    [INCLUDES]
+  configFormat = pkgs.formats.ini {
+    mkKeyValue = generators.mkKeyValueDefault { } " = ";
+  };
 
-    before = paths-nixos.conf
+  mkJailConfig = name: attrs:
+    optionalAttrs (name != "DEFAULT") { inherit (attrs) enabled; } //
+    optionalAttrs (attrs.filter != null) { filter = if (builtins.isString filter) then filter else name; } //
+    attrs.settings;
 
-    ${concatStringsSep "\n" (attrValues (flip mapAttrs cfg.jails (name: def:
-      optionalString (def != "")
-        ''
-          [${name}]
-          ${def}
-        '')))}
-  '';
+  mkFilter = name: attrs: nameValuePair "fail2ban/filter.d/${name}.conf" {
+    source = configFormat.generate "filter.d/${name}.conf" attrs.filter;
+  };
+
+  fail2banConf = configFormat.generate "fail2ban.local" cfg.daemonSettings;
+
+  strJails = filterAttrs (_: builtins.isString) cfg.jails;
+  attrsJails = filterAttrs (_: builtins.isAttrs) cfg.jails;
+
+  jailConf =
+    let
+      configFile = configFormat.generate "jail.local" (
+        { INCLUDES.before = "paths-nixos.conf"; } // (mapAttrs mkJailConfig attrsJails)
+      );
+      extraConfig = concatStringsSep "\n" (attrValues (mapAttrs
+        (name: def:
+          optionalString (def != "")
+            ''
+              [${name}]
+              ${def}
+            '')
+        strJails));
+
+    in
+    pkgs.concatText "jail.local" [ configFile (pkgs.writeText "extra-jail.local" extraConfig) ];
 
   pathsConf = pkgs.writeText "paths-nixos.conf" ''
     # NixOS
@@ -32,15 +53,18 @@ let
 
     [DEFAULT]
   '';
-
 in
 
 {
 
+  imports = [
+    (mkRemovedOptionModule [ "services" "fail2ban" "daemonConfig" ] "The daemon is now configured through the attribute set `services.fail2ban.daemonSettings`.")
+    (mkRemovedOptionModule [ "services" "fail2ban" "extraSettings" ] "The extra default configuration can now be set using `services.fail2ban.jails.DEFAULT.settings`.")
+  ];
+
   ###### interface
 
   options = {
-
     services.fail2ban = {
       enable = mkOption {
         default = false;
@@ -62,15 +86,14 @@ in
       };
 
       packageFirewall = mkOption {
-        default = pkgs.iptables;
-        defaultText = literalExpression "pkgs.iptables";
+        default = config.networking.firewall.package;
+        defaultText = literalExpression "config.networking.firewall.package";
         type = types.package;
-        example = literalExpression "pkgs.nftables";
-        description = lib.mdDoc "The firewall package used by fail2ban service.";
+        description = lib.mdDoc "The firewall package used by fail2ban service. Defaults to the package for your firewall (iptables or nftables).";
       };
 
       extraPackages = mkOption {
-        default = [];
+        default = [ ];
         type = types.listOf types.package;
         example = lib.literalExpression "[ pkgs.ipset ]";
         description = lib.mdDoc ''
@@ -79,6 +102,13 @@ in
         '';
       };
 
+      bantime = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        example = "10m";
+        description = lib.mdDoc "Number of seconds that a host is banned.";
+      };
+
       maxretry = mkOption {
         default = 3;
         type = types.ints.unsigned;
@@ -86,24 +116,24 @@ in
       };
 
       banaction = mkOption {
-        default = "iptables-multiport";
+        default = if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport";
+        defaultText = literalExpression ''if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport"'';
         type = types.str;
-        example = "nftables-multiport";
         description = lib.mdDoc ''
           Default banning action (e.g. iptables, iptables-new, iptables-multiport,
-          iptables-ipset-proto6-allports, shorewall, etc) It is used to
+          iptables-ipset-proto6-allports, shorewall, etc). It is used to
           define action_* variables. Can be overridden globally or per
           section within jail.local file
         '';
       };
 
       banaction-allports = mkOption {
-        default = "iptables-allport";
+        default = if config.networking.nftables.enable then "nftables-allport" else "iptables-allport";
+        defaultText = literalExpression ''if config.networking.nftables.enable then "nftables-allport" else "iptables-allport"'';
         type = types.str;
-        example = "nftables-allport";
         description = lib.mdDoc ''
           Default banning action (e.g. iptables, iptables-new, iptables-multiport,
-          shorewall, etc) It is used to define action_* variables. Can be overridden
+          shorewall, etc) for "allports" jails. It is used to define action_* variables. Can be overridden
           globally or per section within jail.local file
         '';
       };
@@ -112,56 +142,56 @@ in
         default = false;
         type = types.bool;
         description = lib.mdDoc ''
-          Allows to use database for searching of previously banned ip's to increase
-          a default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32...
+          "bantime.increment" allows to use database for searching of previously banned ip's to increase
+          a default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32 ...
         '';
       };
 
       bantime-increment.rndtime = mkOption {
-        default = "4m";
-        type = types.str;
+        default = null;
+        type = types.nullOr types.str;
         example = "8m";
         description = lib.mdDoc ''
-          "bantime-increment.rndtime" is the max number of seconds using for mixing with random time
+          "bantime.rndtime" is the max number of seconds using for mixing with random time
           to prevent "clever" botnets calculate exact time IP can be unbanned again
         '';
       };
 
       bantime-increment.maxtime = mkOption {
-        default = "10h";
-        type = types.str;
+        default = null;
+        type = types.nullOr types.str;
         example = "48h";
         description = lib.mdDoc ''
-          "bantime-increment.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
+          "bantime.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
         '';
       };
 
       bantime-increment.factor = mkOption {
-        default = "1";
-        type = types.str;
+        default = null;
+        type = types.nullOr types.str;
         example = "4";
         description = lib.mdDoc ''
-          "bantime-increment.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
+          "bantime.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
           default value of factor is 1 and with default value of formula, the ban time grows by 1, 2, 4, 8, 16 ...
         '';
       };
 
       bantime-increment.formula = mkOption {
-        default = "ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor";
-        type = types.str;
+        default = null;
+        type = types.nullOr types.str;
         example = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
         description = lib.mdDoc ''
-          "bantime-increment.formula" used by default to calculate next value of ban time, default value bellow,
-          the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32...
+          "bantime.formula" used by default to calculate next value of ban time, default value bellow,
+          the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32 ...
         '';
       };
 
       bantime-increment.multipliers = mkOption {
-        default = "1 2 4 8 16 32 64";
-        type = types.str;
-        example = "2 4 16 128";
+        default = null;
+        type = types.nullOr types.str;
+        example = "1 2 4 8 16 32 64";
         description = lib.mdDoc ''
-          "bantime-increment.multipliers" used to calculate next value of ban time instead of formula, corresponding
+          "bantime.multipliers" used to calculate next value of ban time instead of formula, corresponding
           previously ban count and given "bantime.factor" (for multipliers default is 1);
           following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
           always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
@@ -169,12 +199,12 @@ in
       };
 
       bantime-increment.overalljails = mkOption {
-        default = false;
-        type = types.bool;
+        default = null;
+        type = types.nullOr types.bool;
         example = true;
         description = lib.mdDoc ''
-          "bantime-increment.overalljails"  (if true) specifies the search of IP in the database will be executed
-          cross over all jails, if false (default), only current jail of the ban IP will be searched
+          "bantime.overalljails" (if true) specifies the search of IP in the database will be executed
+          cross over all jails, if false (default), only current jail of the ban IP will be searched.
         '';
       };
 
@@ -188,46 +218,75 @@ in
         '';
       };
 
-      daemonConfig = mkOption {
-        default = ''
-          [Definition]
-          logtarget = SYSLOG
-          socket    = /run/fail2ban/fail2ban.sock
-          pidfile   = /run/fail2ban/fail2ban.pid
-          dbfile    = /var/lib/fail2ban/fail2ban.sqlite3
+      daemonSettings = mkOption {
+        inherit (configFormat) type;
+
+        defaultText = literalExpression ''
+          {
+            Definition = {
+              logtarget = "SYSLOG";
+              socket = "/run/fail2ban/fail2ban.sock";
+              pidfile = "/run/fail2ban/fail2ban.pid";
+              dbfile = "/var/lib/fail2ban/fail2ban.sqlite3";
+            };
+          }
         '';
-        type = types.lines;
         description = lib.mdDoc ''
-          The contents of Fail2ban's main configuration file.  It's
-          generally not necessary to change it.
-       '';
+          The contents of Fail2ban's main configuration file.
+          It's generally not necessary to change it.
+        '';
       };
 
       jails = mkOption {
         default = { };
         example = literalExpression ''
-          { apache-nohome-iptables = '''
-              # Block an IP address if it accesses a non-existent
-              # home directory more than 5 times in 10 minutes,
-              # since that indicates that it's scanning.
-              filter   = apache-nohome
-              action   = iptables-multiport[name=HTTP, port="http,https"]
-              logpath  = /var/log/httpd/error_log*
-              backend = auto
-              findtime = 600
-              bantime  = 600
-              maxretry = 5
-            ''';
-           dovecot = '''
-             # block IPs which failed to log-in
-             # aggressive mode add blocking for aborted connections
-             enabled = true
-             filter = dovecot[mode=aggressive]
-             maxretry = 3
-           ''';
-          }
+          {
+            apache-nohome-iptables = {
+              settings = {
+                # Block an IP address if it accesses a non-existent
+                # home directory more than 5 times in 10 minutes,
+                # since that indicates that it's scanning.
+                filter = "apache-nohome";
+                action = '''iptables-multiport[name=HTTP, port="http,https"]''';
+                logpath = "/var/log/httpd/error_log*";
+                backend = "auto";
+                findtime = 600;
+                bantime = 600;
+                maxretry = 5;
+              };
+            };
+            dovecot = {
+              settings = {
+                # block IPs which failed to log-in
+                # aggressive mode add blocking for aborted connections
+                filter = "dovecot[mode=aggressive]";
+                maxretry = 3;
+              };
+            };
+          };
         '';
-        type = types.attrsOf types.lines;
+        type = with types; attrsOf (either lines (submodule ({ name, ... }: {
+          options = {
+            enabled = mkEnableOption "this jail." // {
+              default = true;
+              readOnly = name == "DEFAULT";
+            };
+
+            filter = mkOption {
+              type = nullOr (either str configFormat.type);
+
+              default = null;
+              description = lib.mdDoc "Content of the filter used for this jail.";
+            };
+
+            settings = mkOption {
+              inherit (settingsFormat) type;
+
+              default = { };
+              description = lib.mdDoc "Additional settings for this jail.";
+            };
+          };
+        })));
         description = lib.mdDoc ''
           The configuration of each Fail2ban “jail”.  A jail
           consists of an action (such as blocking a port using
@@ -256,8 +315,16 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.bantime-increment.formula == null || cfg.bantime-increment.multipliers == null;
+        message = ''
+          Options `services.fail2ban.bantime-increment.formula` and `services.fail2ban.bantime-increment.multipliers` cannot be both specified.
+        '';
+      }
+    ];
 
-    warnings = mkIf (config.networking.firewall.enable == false && config.networking.nftables.enable == false) [
+    warnings = mkIf (!config.networking.firewall.enable && !config.networking.nftables.enable) [
       "fail2ban can not be used without a firewall"
     ];
 
@@ -272,28 +339,18 @@ in
       "fail2ban/paths-nixos.conf".source = pathsConf;
       "fail2ban/action.d".source = "${cfg.package}/etc/fail2ban/action.d/*.conf";
       "fail2ban/filter.d".source = "${cfg.package}/etc/fail2ban/filter.d/*.conf";
-    };
+    } // (mapAttrs' mkFilter (filterAttrs (_: v: v.filter != null && !builtins.isString v.filter) attrsJails));
 
+    systemd.packages = [ cfg.package ];
     systemd.services.fail2ban = {
-      description = "Fail2ban Intrusion Prevention System";
-
       wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
       partOf = optional config.networking.firewall.enable "firewall.service";
 
       restartTriggers = [ fail2banConf jailConf pathsConf ];
 
       path = [ cfg.package cfg.packageFirewall pkgs.iproute2 ] ++ cfg.extraPackages;
 
-      unitConfig.Documentation = "man:fail2ban(1)";
-
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/fail2ban-server -xf start";
-        ExecStop = "${cfg.package}/bin/fail2ban-server stop";
-        ExecReload = "${cfg.package}/bin/fail2ban-server reload";
-        Type = "simple";
-        Restart = "on-failure";
-        PIDFile = "/run/fail2ban/fail2ban.pid";
         # Capabilities
         CapabilityBoundingSet = [ "CAP_AUDIT_READ" "CAP_DAC_READ_SEARCH" "CAP_NET_ADMIN" "CAP_NET_RAW" ];
         # Security
@@ -317,33 +374,41 @@ in
       };
     };
 
+    # Defaults for the daemon settings
+    services.fail2ban.daemonSettings.Definition = {
+      logtarget = mkDefault "SYSLOG";
+      socket = mkDefault "/run/fail2ban/fail2ban.sock";
+      pidfile = mkDefault "/run/fail2ban/fail2ban.pid";
+      dbfile = mkDefault "/var/lib/fail2ban/fail2ban.sqlite3";
+    };
+
     # Add some reasonable default jails.  The special "DEFAULT" jail
     # sets default values for all other jails.
-    services.fail2ban.jails.DEFAULT = ''
-      ${optionalString cfg.bantime-increment.enable ''
-        # Bantime incremental
-        bantime.increment    = ${boolToString cfg.bantime-increment.enable}
-        bantime.maxtime      = ${cfg.bantime-increment.maxtime}
-        bantime.factor       = ${cfg.bantime-increment.factor}
-        bantime.formula      = ${cfg.bantime-increment.formula}
-        bantime.multipliers  = ${cfg.bantime-increment.multipliers}
-        bantime.overalljails = ${boolToString cfg.bantime-increment.overalljails}
-      ''}
-      # Miscellaneous options
-      ignoreip    = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}
-      maxretry    = ${toString cfg.maxretry}
-      backend     = systemd
-      # Actions
-      banaction   = ${cfg.banaction}
-      banaction_allports = ${cfg.banaction-allports}
-    '';
-    # Block SSH if there are too many failing connection attempts.
+    services.fail2ban.jails = mkMerge [
+      {
+        DEFAULT.settings = (optionalAttrs cfg.bantime-increment.enable
+          ({ "bantime.increment" = cfg.bantime-increment.enable; } // (mapAttrs'
+            (name: nameValuePair "bantime.${name}")
+            (filterAttrs (n: v: v != null && n != "enable") cfg.bantime-increment))
+          )
+        ) // {
+          # Miscellaneous options
+          inherit (cfg) banaction maxretry;
+          ignoreip = ''127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}'';
+          backend = "systemd";
+          # Actions
+          banaction_allports = cfg.banaction-allports;
+        };
+      }
+
+      # Block SSH if there are too many failing connection attempts.
+      (mkIf config.services.openssh.enable {
+        sshd.settings.port = mkDefault (concatMapStringsSep "," builtins.toString config.services.openssh.ports);
+      })
+    ];
+
     # Benefits from verbose sshd logging to observe failed login attempts,
     # so we set that here unless the user overrode it.
-    services.openssh.logLevel = lib.mkDefault "VERBOSE";
-    services.fail2ban.jails.sshd = mkDefault ''
-      enabled = true
-      port    = ${concatMapStringsSep "," (p: toString p) config.services.openssh.ports}
-    '';
+    services.openssh.settings.LogLevel = mkDefault "VERBOSE";
   };
 }
diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix
index 55120799c99..cea2a56bdcd 100644
--- a/nixos/modules/services/security/kanidm.nix
+++ b/nixos/modules/services/security/kanidm.nix
@@ -7,6 +7,18 @@ let
   serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings);
   clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings);
   unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings);
+  certPaths = builtins.map builtins.dirOf [ cfg.serverSettings.tls_chain cfg.serverSettings.tls_key ];
+
+  # Merge bind mount paths and remove paths where a prefix is already mounted.
+  # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is already in the mount
+  # paths, no new bind mount is added. Adding subpaths caused problems on ofborg.
+  hasPrefixInList = list: newPath: lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list;
+  mergePaths = lib.foldl' (merged: newPath: let
+      # If the new path is a prefix to some existing path, we need to filter it out
+      filteredPaths = lib.filter (p: !lib.hasPrefix (builtins.toString newPath) (builtins.toString p)) merged;
+      # If a prefix of the new path is already in the list, do not add it
+      filteredNew = lib.optional (!hasPrefixInList filteredPaths newPath) newPath;
+    in filteredPaths ++ filteredNew) [];
 
   defaultServiceConfig = {
     BindReadOnlyPaths = [
@@ -16,7 +28,7 @@ let
       "-/etc/hosts"
       "-/etc/localtime"
     ];
-    CapabilityBoundingSet = "";
+    CapabilityBoundingSet = [];
     # ProtectClock= adds DeviceAllow=char-rtc r
     DeviceAllow = "";
     # Implies ProtectSystem=strict, which re-mounts all paths
@@ -55,7 +67,7 @@ in
   options.services.kanidm = {
     enableClient = lib.mkEnableOption (lib.mdDoc "the Kanidm client");
     enableServer = lib.mkEnableOption (lib.mdDoc "the Kanidm server");
-    enablePam = lib.mkEnableOption (lib.mdDoc "the Kanidm PAM and NSS integration.");
+    enablePam = lib.mkEnableOption (lib.mdDoc "the Kanidm PAM and NSS integration");
 
     serverSettings = lib.mkOption {
       type = lib.types.submodule {
@@ -216,22 +228,28 @@ in
       description = "kanidm identity management daemon";
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
-      serviceConfig = defaultServiceConfig // {
-        StateDirectory = "kanidm";
-        StateDirectoryMode = "0700";
-        ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}";
-        User = "kanidm";
-        Group = "kanidm";
+      serviceConfig = lib.mkMerge [
+        # Merge paths and ignore existing prefixes needs to sidestep mkMerge
+        (defaultServiceConfig // {
+          BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths);
+        })
+        {
+          StateDirectory = "kanidm";
+          StateDirectoryMode = "0700";
+          ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}";
+          User = "kanidm";
+          Group = "kanidm";
 
-        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
-        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
-        # This would otherwise override the CAP_NET_BIND_SERVICE capability.
-        PrivateUsers = false;
-        # Port needs to be exposed to the host network
-        PrivateNetwork = false;
-        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
-        TemporaryFileSystem = "/:ro";
-      };
+          AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+          CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+          # This would otherwise override the CAP_NET_BIND_SERVICE capability.
+          PrivateUsers = lib.mkForce false;
+          # Port needs to be exposed to the host network
+          PrivateNetwork = lib.mkForce false;
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          TemporaryFileSystem = "/:ro";
+        }
+      ];
       environment.RUST_LOG = "info";
     };
 
@@ -240,34 +258,32 @@ in
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       restartTriggers = [ unixConfigFile clientConfigFile ];
-      serviceConfig = defaultServiceConfig // {
-        CacheDirectory = "kanidm-unixd";
-        CacheDirectoryMode = "0700";
-        RuntimeDirectory = "kanidm-unixd";
-        ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd";
-        User = "kanidm-unixd";
-        Group = "kanidm-unixd";
+      serviceConfig = lib.mkMerge [
+        defaultServiceConfig
+        {
+          CacheDirectory = "kanidm-unixd";
+          CacheDirectoryMode = "0700";
+          RuntimeDirectory = "kanidm-unixd";
+          ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd";
+          User = "kanidm-unixd";
+          Group = "kanidm-unixd";
 
-        BindReadOnlyPaths = [
-          "/nix/store"
-          "-/etc/resolv.conf"
-          "-/etc/nsswitch.conf"
-          "-/etc/hosts"
-          "-/etc/localtime"
-          "-/etc/kanidm"
-          "-/etc/static/kanidm"
-          "-/etc/ssl"
-          "-/etc/static/ssl"
-        ];
-        BindPaths = [
-          # To create the socket
-          "/run/kanidm-unixd:/var/run/kanidm-unixd"
-        ];
-        # Needs to connect to kanidmd
-        PrivateNetwork = false;
-        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
-        TemporaryFileSystem = "/:ro";
-      };
+          BindReadOnlyPaths = [
+            "-/etc/kanidm"
+            "-/etc/static/kanidm"
+            "-/etc/ssl"
+            "-/etc/static/ssl"
+          ];
+          BindPaths = [
+            # To create the socket
+            "/run/kanidm-unixd:/var/run/kanidm-unixd"
+          ];
+          # Needs to connect to kanidmd
+          PrivateNetwork = lib.mkForce false;
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+          TemporaryFileSystem = "/:ro";
+        }
+      ];
       environment.RUST_LOG = "info";
     };
 
@@ -304,6 +320,7 @@ in
         ProtectHome = false;
         RestrictAddressFamilies = [ "AF_UNIX" ];
         TemporaryFileSystem = "/:ro";
+        Restart = "on-failure";
       };
       environment.RUST_LOG = "info";
     };
diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix
index e3f8e75ca24..12547acabfe 100644
--- a/nixos/modules/services/security/oauth2_proxy.nix
+++ b/nixos/modules/services/security/oauth2_proxy.nix
@@ -72,15 +72,14 @@ let
   } // (getProviderOptions cfg cfg.provider) // cfg.extraConfig;
 
   mapConfig = key: attr:
-  if attr != null && attr != [] then (
+  optionalString (attr != null && attr != []) (
     if isDerivation attr then mapConfig key (toString attr) else
     if (builtins.typeOf attr) == "set" then concatStringsSep " "
       (mapAttrsToList (name: value: mapConfig (key + "-" + name) value) attr) else
     if (builtins.typeOf attr) == "list" then concatMapStringsSep " " (mapConfig key) attr else
     if (builtins.typeOf attr) == "bool" then "--${key}=${boolToString attr}" else
     if (builtins.typeOf attr) == "string" then "--${key}='${attr}'" else
-    "--${key}=${toString attr}")
-    else "";
+    "--${key}=${toString attr}");
 
   configString = concatStringsSep " " (mapAttrsToList mapConfig allConfig);
 in
diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix
index e446e606cad..664335cb58e 100644
--- a/nixos/modules/services/security/privacyidea.nix
+++ b/nixos/modules/services/security/privacyidea.nix
@@ -6,7 +6,7 @@ let
   cfg = config.services.privacyidea;
   opt = options.services.privacyidea;
 
-  uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; python3 = pkgs.python39; };
+  uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; python3 = pkgs.python310; };
   python = uwsgi.python3;
   penv = python.withPackages (const [ pkgs.privacyidea ]);
   logCfg = pkgs.writeText "privacyidea-log.cfg" ''
@@ -41,7 +41,7 @@ let
 
   piCfgFile = pkgs.writeText "privacyidea.cfg" ''
     SUPERUSER_REALM = [ '${concatStringsSep "', '" cfg.superuserRealm}' ]
-    SQLALCHEMY_DATABASE_URI = 'postgresql:///privacyidea'
+    SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2:///privacyidea'
     SECRET_KEY = '${cfg.secretKey}'
     PI_PEPPER = '${cfg.pepper}'
     PI_ENCFILE = '${cfg.encFile}'
diff --git a/nixos/modules/services/security/tor.nix b/nixos/modules/services/security/tor.nix
index 2aa2964f881..9e786eb2bf0 100644
--- a/nixos/modules/services/security/tor.nix
+++ b/nixos/modules/services/security/tor.nix
@@ -769,7 +769,7 @@ in
           };
           options.SOCKSPort = mkOption {
             description = lib.mdDoc (descriptionGeneric "SOCKSPort");
-            default = if cfg.settings.HiddenServiceNonAnonymousMode == true then [{port = 0;}] else [];
+            default = lib.optionals cfg.settings.HiddenServiceNonAnonymousMode [{port = 0;}];
             defaultText = literalExpression ''
               if config.${opt.settings}.HiddenServiceNonAnonymousMode == true
               then [ { port = 0; } ]
@@ -897,8 +897,7 @@ in
       allowedTCPPorts =
         concatMap (o:
           if isInt o && o > 0 then [o]
-          else if o ? "port" && isInt o.port && o.port > 0 then [o.port]
-          else []
+          else optionals (o ? "port" && isInt o.port && o.port > 0) [o.port]
         ) (flatten [
           cfg.settings.ORPort
           cfg.settings.DirPort
diff --git a/nixos/modules/services/security/usbguard.nix b/nixos/modules/services/security/usbguard.nix
index 1d846b19407..651f5255ac8 100644
--- a/nixos/modules/services/security/usbguard.nix
+++ b/nixos/modules/services/security/usbguard.nix
@@ -150,6 +150,8 @@ in
           Generate device specific rules including the "via-port" attribute.
         '';
       };
+
+      dbus.enable = mkEnableOption (lib.mdDoc "USBGuard dbus daemon");
     };
   };
 
@@ -160,49 +162,90 @@ in
 
     environment.systemPackages = [ cfg.package ];
 
-    systemd.services.usbguard = {
-      description = "USBGuard daemon";
-
-      wantedBy = [ "basic.target" ];
-      wants = [ "systemd-udevd.service" ];
-
-      # make sure an empty rule file exists
-      preStart = ''[ -f "${ruleFile}" ] || touch ${ruleFile}'';
-
-      serviceConfig = {
-        Type = "simple";
-        ExecStart = "${cfg.package}/bin/usbguard-daemon -P -k -c ${daemonConfFile}";
-        Restart = "on-failure";
-
-        StateDirectory = [
-          "usbguard"
-          "usbguard/IPCAccessControl.d"
-        ];
-
-        AmbientCapabilities = "";
-        CapabilityBoundingSet = "CAP_CHOWN CAP_FOWNER";
-        DeviceAllow = "/dev/null rw";
-        DevicePolicy = "strict";
-        IPAddressDeny = "any";
-        LockPersonality = true;
-        MemoryDenyWriteExecute = true;
-        NoNewPrivileges = true;
-        PrivateDevices = true;
-        PrivateTmp = true;
-        ProtectControlGroups = true;
-        ProtectHome = true;
-        ProtectKernelModules = true;
-        ProtectSystem = true;
-        ReadOnlyPaths = "-/";
-        ReadWritePaths = "-/dev/shm -/tmp";
-        RestrictAddressFamilies = [ "AF_UNIX" "AF_NETLINK" ];
-        RestrictNamespaces = true;
-        RestrictRealtime = true;
-        SystemCallArchitectures = "native";
-        SystemCallFilter = "@system-service";
-        UMask = "0077";
+    systemd.services = {
+      usbguard = {
+        description = "USBGuard daemon";
+
+        wantedBy = [ "basic.target" ];
+        wants = [ "systemd-udevd.service" ];
+
+        # make sure an empty rule file exists
+        preStart = ''[ -f "${ruleFile}" ] || touch ${ruleFile}'';
+
+        serviceConfig = {
+          Type = "simple";
+          ExecStart = "${cfg.package}/bin/usbguard-daemon -P -k -c ${daemonConfFile}";
+          Restart = "on-failure";
+
+          StateDirectory = [
+            "usbguard"
+            "usbguard/IPCAccessControl.d"
+          ];
+
+          AmbientCapabilities = "";
+          CapabilityBoundingSet = "CAP_CHOWN CAP_FOWNER";
+          DeviceAllow = "/dev/null rw";
+          DevicePolicy = "strict";
+          IPAddressDeny = "any";
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
+          PrivateDevices = true;
+          PrivateTmp = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectKernelModules = true;
+          ProtectSystem = true;
+          ReadOnlyPaths = "-/";
+          ReadWritePaths = "-/dev/shm -/tmp";
+          RestrictAddressFamilies = [ "AF_UNIX" "AF_NETLINK" ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = "@system-service";
+          UMask = "0077";
+        };
+      };
+
+      usbguard-dbus = mkIf cfg.dbus.enable {
+        description = "USBGuard D-Bus Service";
+
+        wantedBy = [ "multi-user.target" ];
+        requires = [ "usbguard.service" ];
+
+        serviceConfig = {
+          Type = "dbus";
+          BusName = "org.usbguard1";
+          ExecStart = "${cfg.package}/bin/usbguard-dbus --system";
+          Restart = "on-failure";
+        };
+
+        aliases = [ "dbus-org.usbguard.service" ];
       };
     };
+
+    security.polkit.extraConfig =
+      let
+        groupCheck = (lib.concatStrings (map
+          (g: "subject.isInGroup(\"${g}\") || ")
+          cfg.IPCAllowedGroups))
+        + "false";
+      in
+      optionalString cfg.dbus.enable ''
+        polkit.addRule(function(action, subject) {
+            if ((action.id == "org.usbguard.Policy1.listRules" ||
+                 action.id == "org.usbguard.Policy1.appendRule" ||
+                 action.id == "org.usbguard.Policy1.removeRule" ||
+                 action.id == "org.usbguard.Devices1.applyDevicePolicy" ||
+                 action.id == "org.usbguard.Devices1.listDevices" ||
+                 action.id == "org.usbguard1.getParameter" ||
+                 action.id == "org.usbguard1.setParameter") &&
+                subject.active == true && subject.local == true &&
+                (${groupCheck})) {
+                    return polkit.Result.YES;
+            }
+        });
+      '';
   };
   imports = [
     (mkRemovedOptionModule [ "services" "usbguard" "ruleFile" ] "The usbguard module now uses ${defaultRuleFile} as ruleFile. Alternatively, use services.usbguard.rules to configure rules.")
diff --git a/nixos/modules/services/security/vault-agent.nix b/nixos/modules/services/security/vault-agent.nix
new file mode 100644
index 00000000000..17b8ff83592
--- /dev/null
+++ b/nixos/modules/services/security/vault-agent.nix
@@ -0,0 +1,128 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  format = pkgs.formats.json { };
+  commonOptions = { pkgName, flavour ? pkgName }: mkOption {
+    default = { };
+    description = mdDoc ''
+      Attribute set of ${flavour} instances.
+      Creates independent `${flavour}-''${name}.service` systemd units for each instance defined here.
+    '';
+    type = with types; attrsOf (submodule ({ name, ... }: {
+      options = {
+        enable = mkEnableOption (mdDoc "this ${flavour} instance") // { default = true; };
+
+        package = mkPackageOptionMD pkgs pkgName { };
+
+        user = mkOption {
+          type = types.str;
+          default = "root";
+          description = mdDoc ''
+            User under which this instance runs.
+          '';
+        };
+
+        group = mkOption {
+          type = types.str;
+          default = "root";
+          description = mdDoc ''
+            Group under which this instance runs.
+          '';
+        };
+
+        settings = mkOption {
+          type = types.submodule {
+            freeformType = format.type;
+
+            options = {
+              pid_file = mkOption {
+                default = "/run/${flavour}/${name}.pid";
+                type = types.str;
+                description = mdDoc ''
+                  Path to use for the pid file.
+                '';
+              };
+
+              template = mkOption {
+                default = [ ];
+                type = with types; listOf (attrsOf anything);
+                description =
+                  let upstreamDocs =
+                    if flavour == "vault-agent"
+                    then "https://developer.hashicorp.com/vault/docs/agent/template"
+                    else "https://github.com/hashicorp/consul-template/blob/main/docs/configuration.md#templates";
+                  in
+                  mdDoc ''
+                    Template section of ${flavour}.
+                    Refer to <${upstreamDocs}> for supported values.
+                  '';
+              };
+            };
+          };
+
+          default = { };
+
+          description =
+            let upstreamDocs =
+              if flavour == "vault-agent"
+              then "https://developer.hashicorp.com/vault/docs/agent#configuration-file-options"
+              else "https://github.com/hashicorp/consul-template/blob/main/docs/configuration.md#configuration-file";
+            in
+            mdDoc ''
+              Free-form settings written directly to the `config.json` file.
+              Refer to <${upstreamDocs}> for supported values.
+
+              ::: {.note}
+              Resulting format is JSON not HCL.
+              Refer to <https://www.hcl2json.com/> if you are unsure how to convert HCL options to JSON.
+              :::
+            '';
+        };
+      };
+    }));
+  };
+
+  createAgentInstance = { instance, name, flavour }:
+    let
+      configFile = format.generate "${name}.json" instance.settings;
+    in
+    mkIf (instance.enable) {
+      description = "${flavour} daemon - ${name}";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = [ pkgs.getent ];
+      startLimitIntervalSec = 60;
+      startLimitBurst = 3;
+      serviceConfig = {
+        User = instance.user;
+        Group = instance.group;
+        RuntimeDirectory = flavour;
+        ExecStart = "${getExe instance.package} ${optionalString ((getName instance.package) == "vault") "agent"} -config ${configFile}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
+        KillSignal = "SIGINT";
+        TimeoutStopSec = "30s";
+        Restart = "on-failure";
+      };
+    };
+in
+{
+  options = {
+    services.consul-template.instances = commonOptions { pkgName = "consul-template"; };
+    services.vault-agent.instances = commonOptions { pkgName = "vault"; flavour = "vault-agent"; };
+  };
+
+  config = mkMerge (map
+    (flavour:
+      let cfg = config.services.${flavour}; in
+      mkIf (cfg.instances != { }) {
+        systemd.services = mapAttrs'
+          (name: instance: nameValuePair "${flavour}-${name}" (createAgentInstance { inherit name instance flavour; }))
+          cfg.instances;
+      })
+    [ "consul-template" "vault-agent" ]);
+
+  meta.maintainers = with maintainers; [ emilylange tcheronneau ];
+}
+
diff --git a/nixos/modules/services/security/vault.nix b/nixos/modules/services/security/vault.nix
index 7b9e31a8d99..18d981cdb0d 100644
--- a/nixos/modules/services/security/vault.nix
+++ b/nixos/modules/services/security/vault.nix
@@ -221,6 +221,7 @@ in
         ProtectHome = "read-only";
         AmbientCapabilities = "cap_ipc_lock";
         NoNewPrivileges = true;
+        LimitCORE = 0;
         KillSignal = "SIGINT";
         TimeoutStopSec = "30s";
         Restart = "on-failure";
diff --git a/nixos/modules/services/security/vaultwarden/default.nix b/nixos/modules/services/security/vaultwarden/default.nix
index aaa3f5507f7..98ab8595bdd 100644
--- a/nixos/modules/services/security/vaultwarden/default.nix
+++ b/nixos/modules/services/security/vaultwarden/default.nix
@@ -59,7 +59,12 @@ in {
 
     config = mkOption {
       type = attrsOf (nullOr (oneOf [ bool int str ]));
-      default = {};
+      default = {
+        config = {
+          ROCKET_ADDRESS = "::1"; # default to localhost
+          ROCKET_PORT = 8222;
+        };
+      };
       example = literalExpression ''
         {
           DOMAIN = "https://bitwarden.example.com";
diff --git a/nixos/modules/services/security/yubikey-agent.nix b/nixos/modules/services/security/yubikey-agent.nix
index c91ff3e69a0..ee57ec8bf81 100644
--- a/nixos/modules/services/security/yubikey-agent.nix
+++ b/nixos/modules/services/security/yubikey-agent.nix
@@ -57,6 +57,9 @@ in
       ];
     };
 
+    # Yubikey-agent expects pcsd to be running in order to function.
+    services.pcscd.enable = true;
+
     environment.extraInit = ''
       if [ -z "$SSH_AUTH_SOCK" -a -n "$XDG_RUNTIME_DIR" ]; then
         export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/yubikey-agent/yubikey-agent.sock"
diff --git a/nixos/modules/services/system/cachix-agent/default.nix b/nixos/modules/services/system/cachix-agent/default.nix
index 11769d4e309..06494ddb631 100644
--- a/nixos/modules/services/system/cachix-agent/default.nix
+++ b/nixos/modules/services/system/cachix-agent/default.nix
@@ -72,7 +72,7 @@ in {
         EnvironmentFile = cfg.credentialsFile;
         ExecStart = ''
           ${cfg.package}/bin/cachix ${lib.optionalString cfg.verbose "--verbose"} ${lib.optionalString (cfg.host != null) "--host ${cfg.host}"} \
-            deploy agent ${cfg.name} ${if cfg.profile != null then cfg.profile else ""}
+            deploy agent ${cfg.name} ${optionalString (cfg.profile != null) cfg.profile}
         '';
       };
     };
diff --git a/nixos/modules/services/system/cachix-watch-store.nix b/nixos/modules/services/system/cachix-watch-store.nix
index ec73c0bcdcf..89157b460b9 100644
--- a/nixos/modules/services/system/cachix-watch-store.nix
+++ b/nixos/modules/services/system/cachix-watch-store.nix
@@ -25,7 +25,7 @@ in
 
     compressionLevel = mkOption {
       type = types.nullOr types.int;
-      description = lib.mdDoc "The compression level for XZ compression (between 0 and 9)";
+      description = lib.mdDoc "The compression level for ZSTD compression (between 0 and 16)";
       default = null;
     };
 
@@ -62,7 +62,13 @@ in
       after = [ "network-online.target" ];
       path = [ config.nix.package ];
       wantedBy = [ "multi-user.target" ];
+      unitConfig = {
+        # allow to restart indefinitely
+        StartLimitIntervalSec = 0;
+      };
       serviceConfig = {
+        # don't put too much stress on the machine when restarting
+        RestartSec = 1;
         # we don't want to kill children processes as those are deployments
         KillMode = "process";
         Restart = "on-failure";
diff --git a/nixos/modules/services/system/cloud-init.nix b/nixos/modules/services/system/cloud-init.nix
index d75070dea43..d782bb1a366 100644
--- a/nixos/modules/services/system/cloud-init.nix
+++ b/nixos/modules/services/system/cloud-init.nix
@@ -2,17 +2,23 @@
 
 with lib;
 
-let cfg = config.services.cloud-init;
-    path = with pkgs; [
-      cloud-init
-      iproute2
-      nettools
-      openssh
-      shadow
-      util-linux
-    ] ++ optional cfg.btrfs.enable btrfs-progs
-      ++ optional cfg.ext4.enable e2fsprogs
-    ;
+let
+  cfg = config.services.cloud-init;
+  path = with pkgs; [
+    cloud-init
+    iproute2
+    nettools
+    openssh
+    shadow
+    util-linux
+    busybox
+  ]
+  ++ optional cfg.btrfs.enable btrfs-progs
+  ++ optional cfg.ext4.enable e2fsprogs
+  ++ optional cfg.xfs.enable xfsprogs
+  ;
+  settingsFormat = pkgs.formats.yaml { };
+  cfgfile = settingsFormat.generate "cloud.cfg" cfg.settings;
 in
 {
   options = {
@@ -20,7 +26,7 @@ in
       enable = mkOption {
         type = types.bool;
         default = false;
-        description = lib.mdDoc ''
+        description = mdDoc ''
           Enable the cloud-init service. This services reads
           configuration metadata in a cloud environment and configures
           the machine according to this metadata.
@@ -39,7 +45,7 @@ in
       btrfs.enable = mkOption {
         type = types.bool;
         default = false;
-        description = lib.mdDoc ''
+        description = mdDoc ''
           Allow the cloud-init service to operate `btrfs` filesystem.
         '';
       };
@@ -47,70 +53,46 @@ in
       ext4.enable = mkOption {
         type = types.bool;
         default = true;
-        description = lib.mdDoc ''
+        description = mdDoc ''
           Allow the cloud-init service to operate `ext4` filesystem.
         '';
       };
 
+      xfs.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = mdDoc ''
+          Allow the cloud-init service to operate `xfs` filesystem.
+        '';
+      };
+
       network.enable = mkOption {
         type = types.bool;
         default = false;
-        description = lib.mdDoc ''
+        description = mdDoc ''
           Allow the cloud-init service to configure network interfaces
           through systemd-networkd.
         '';
       };
 
+      settings = mkOption {
+        description = mdDoc ''
+          Structured cloud-init configuration.
+        '';
+        type = types.submodule {
+          freeformType = settingsFormat.type;
+        };
+        default = { };
+      };
+
       config = mkOption {
         type = types.str;
-        default = ''
-          system_info:
-            distro: nixos
-            network:
-              renderers: [ 'networkd' ]
-          users:
-             - root
-
-          disable_root: false
-          preserve_hostname: false
-
-          cloud_init_modules:
-           - migrator
-           - seed_random
-           - bootcmd
-           - write-files
-           - growpart
-           - resizefs
-           - update_hostname
-           - resolv_conf
-           - ca-certs
-           - rsyslog
-           - users-groups
-
-          cloud_config_modules:
-           - disk_setup
-           - mounts
-           - ssh-import-id
-           - set-passwords
-           - timezone
-           - disable-ec2-metadata
-           - runcmd
-           - ssh
-
-          cloud_final_modules:
-           - rightscale_userdata
-           - scripts-vendor
-           - scripts-per-once
-           - scripts-per-boot
-           - scripts-per-instance
-           - scripts-user
-           - ssh-authkey-fingerprints
-           - keys-to-console
-           - phone-home
-           - final-message
-           - power-state-change
-          '';
-        description = lib.mdDoc "cloud-init configuration.";
+        default = "";
+        description = mdDoc ''
+          raw cloud-init configuration.
+
+          Takes precedence over the `settings` option if set.
+        '';
       };
 
     };
@@ -118,78 +100,140 @@ in
   };
 
   config = mkIf cfg.enable {
+    services.cloud-init.settings = {
+      system_info = mkDefault {
+        distro = "nixos";
+        network = {
+          renderers = [ "networkd" ];
+        };
+      };
+
+      users = mkDefault [ "root" ];
+      disable_root = mkDefault false;
+      preserve_hostname = mkDefault false;
+
+      cloud_init_modules = mkDefault [
+        "migrator"
+        "seed_random"
+        "bootcmd"
+        "write-files"
+        "growpart"
+        "resizefs"
+        "update_hostname"
+        "resolv_conf"
+        "ca-certs"
+        "rsyslog"
+        "users-groups"
+      ];
+
+      cloud_config_modules = mkDefault [
+        "disk_setup"
+        "mounts"
+        "ssh-import-id"
+        "set-passwords"
+        "timezone"
+        "disable-ec2-metadata"
+        "runcmd"
+        "ssh"
+      ];
+
+      cloud_final_modules = mkDefault [
+        "rightscale_userdata"
+        "scripts-vendor"
+        "scripts-per-once"
+        "scripts-per-boot"
+        "scripts-per-instance"
+        "scripts-user"
+        "ssh-authkey-fingerprints"
+        "keys-to-console"
+        "phone-home"
+        "final-message"
+        "power-state-change"
+      ];
+    };
 
-    environment.etc."cloud/cloud.cfg".text = cfg.config;
+    environment.etc."cloud/cloud.cfg" =
+      if cfg.config == "" then
+        { source = cfgfile; }
+      else
+        { text = cfg.config; }
+    ;
 
     systemd.network.enable = cfg.network.enable;
 
-    systemd.services.cloud-init-local =
-      { description = "Initial cloud-init job (pre-networking)";
-        wantedBy = [ "multi-user.target" ];
-        before = ["systemd-networkd.service"];
-        path = path;
-        serviceConfig =
-          { Type = "oneshot";
-            ExecStart = "${pkgs.cloud-init}/bin/cloud-init init --local";
-            RemainAfterExit = "yes";
-            TimeoutSec = "infinity";
-            StandardOutput = "journal+console";
-          };
+    systemd.services.cloud-init-local = {
+      description = "Initial cloud-init job (pre-networking)";
+      wantedBy = [ "multi-user.target" ];
+      before = [ "systemd-networkd.service" ];
+      path = path;
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.cloud-init}/bin/cloud-init init --local";
+        RemainAfterExit = "yes";
+        TimeoutSec = "infinity";
+        StandardOutput = "journal+console";
       };
+    };
 
-    systemd.services.cloud-init =
-      { description = "Initial cloud-init job (metadata service crawler)";
-        wantedBy = [ "multi-user.target" ];
-        wants = [ "network-online.target" "cloud-init-local.service"
-                  "sshd.service" "sshd-keygen.service" ];
-        after = [ "network-online.target" "cloud-init-local.service" ];
-        before = [ "sshd.service" "sshd-keygen.service" ];
-        requires = [ "network.target"];
-        path = path;
-        serviceConfig =
-          { Type = "oneshot";
-            ExecStart = "${pkgs.cloud-init}/bin/cloud-init init";
-            RemainAfterExit = "yes";
-            TimeoutSec = "infinity";
-            StandardOutput = "journal+console";
-          };
+    systemd.services.cloud-init = {
+      description = "Initial cloud-init job (metadata service crawler)";
+      wantedBy = [ "multi-user.target" ];
+      wants = [
+        "network-online.target"
+        "cloud-init-local.service"
+        "sshd.service"
+        "sshd-keygen.service"
+      ];
+      after = [ "network-online.target" "cloud-init-local.service" ];
+      before = [ "sshd.service" "sshd-keygen.service" ];
+      requires = [ "network.target" ];
+      path = path;
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.cloud-init}/bin/cloud-init init";
+        RemainAfterExit = "yes";
+        TimeoutSec = "infinity";
+        StandardOutput = "journal+console";
       };
+    };
 
-    systemd.services.cloud-config =
-      { description = "Apply the settings specified in cloud-config";
-        wantedBy = [ "multi-user.target" ];
-        wants = [ "network-online.target" ];
-        after = [ "network-online.target" "syslog.target" "cloud-config.target" ];
-
-        path = path;
-        serviceConfig =
-          { Type = "oneshot";
-            ExecStart = "${pkgs.cloud-init}/bin/cloud-init modules --mode=config";
-            RemainAfterExit = "yes";
-            TimeoutSec = "infinity";
-            StandardOutput = "journal+console";
-          };
+    systemd.services.cloud-config = {
+      description = "Apply the settings specified in cloud-config";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" "syslog.target" "cloud-config.target" ];
+
+      path = path;
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.cloud-init}/bin/cloud-init modules --mode=config";
+        RemainAfterExit = "yes";
+        TimeoutSec = "infinity";
+        StandardOutput = "journal+console";
       };
+    };
 
-    systemd.services.cloud-final =
-      { description = "Execute cloud user/final scripts";
-        wantedBy = [ "multi-user.target" ];
-        wants = [ "network-online.target" ];
-        after = [ "network-online.target" "syslog.target" "cloud-config.service" "rc-local.service" ];
-        requires = [ "cloud-config.target" ];
-        path = path;
-        serviceConfig =
-          { Type = "oneshot";
-            ExecStart = "${pkgs.cloud-init}/bin/cloud-init modules --mode=final";
-            RemainAfterExit = "yes";
-            TimeoutSec = "infinity";
-            StandardOutput = "journal+console";
-          };
+    systemd.services.cloud-final = {
+      description = "Execute cloud user/final scripts";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" "syslog.target" "cloud-config.service" "rc-local.service" ];
+      requires = [ "cloud-config.target" ];
+      path = path;
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.cloud-init}/bin/cloud-init modules --mode=final";
+        RemainAfterExit = "yes";
+        TimeoutSec = "infinity";
+        StandardOutput = "journal+console";
       };
+    };
 
-    systemd.targets.cloud-config =
-      { description = "Cloud-config availability";
-        requires = [ "cloud-init-local.service" "cloud-init.service" ];
-      };
+    systemd.targets.cloud-config = {
+      description = "Cloud-config availability";
+      requires = [ "cloud-init-local.service" "cloud-init.service" ];
+    };
   };
+
+  meta.maintainers = [ maintainers.zimbatm ];
 }
diff --git a/nixos/modules/services/system/dbus.nix b/nixos/modules/services/system/dbus.nix
index c677088101f..8d5b25e6176 100644
--- a/nixos/modules/services/system/dbus.nix
+++ b/nixos/modules/services/system/dbus.nix
@@ -14,13 +14,17 @@ let
     serviceDirectories = cfg.packages;
   };
 
-  inherit (lib) mkOption mkIf mkMerge types;
+  inherit (lib) mkOption mkEnableOption mkIf mkMerge types;
 
 in
 
 {
   options = {
 
+    boot.initrd.systemd.dbus = {
+      enable = mkEnableOption (lib.mdDoc "dbus in stage 1");
+    };
+
     services.dbus = {
 
       enable = mkOption {
@@ -111,6 +115,21 @@ in
       ];
     }
 
+    (mkIf config.boot.initrd.systemd.dbus.enable {
+      boot.initrd.systemd = {
+        users.messagebus = { };
+        groups.messagebus = { };
+        contents."/etc/dbus-1".source = pkgs.makeDBusConf {
+          inherit (cfg) apparmor;
+          suidHelper = "/bin/false";
+          serviceDirectories = [ pkgs.dbus ];
+        };
+        packages = [ pkgs.dbus ];
+        storePaths = [ "${pkgs.dbus}/bin/dbus-daemon" ];
+        targets.sockets.wants = [ "dbus.socket" ];
+      };
+    })
+
     (mkIf (cfg.implementation == "dbus") {
       environment.systemPackages = [
         pkgs.dbus
diff --git a/nixos/modules/services/system/nix-daemon.nix b/nixos/modules/services/system/nix-daemon.nix
new file mode 100644
index 00000000000..0d6bec88839
--- /dev/null
+++ b/nixos/modules/services/system/nix-daemon.nix
@@ -0,0 +1,262 @@
+/*
+  Declares what makes the nix-daemon work on systemd.
+
+  See also
+   - nixos/modules/config/nix.nix: the nix.conf
+   - nixos/modules/config/nix-remote-build.nix: the nix.conf
+*/
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.nix;
+
+  nixPackage = cfg.package.out;
+
+  isNixAtLeast = versionAtLeast (getVersion nixPackage);
+
+  makeNixBuildUser = nr: {
+    name = "nixbld${toString nr}";
+    value = {
+      description = "Nix build user ${toString nr}";
+
+      /*
+        For consistency with the setgid(2), setuid(2), and setgroups(2)
+        calls in `libstore/build.cc', don't add any supplementary group
+        here except "nixbld".
+      */
+      uid = builtins.add config.ids.uids.nixbld nr;
+      isSystemUser = true;
+      group = "nixbld";
+      extraGroups = [ "nixbld" ];
+    };
+  };
+
+  nixbldUsers = listToAttrs (map makeNixBuildUser (range 1 cfg.nrBuildUsers));
+
+in
+
+{
+  imports = [
+    (mkRenamedOptionModuleWith { sinceRelease = 2205; from = [ "nix" "daemonIONiceLevel" ]; to = [ "nix" "daemonIOSchedPriority" ]; })
+    (mkRenamedOptionModuleWith { sinceRelease = 2211; from = [ "nix" "readOnlyStore" ]; to = [ "boot" "readOnlyNixStore" ]; })
+    (mkRemovedOptionModule [ "nix" "daemonNiceLevel" ] "Consider nix.daemonCPUSchedPolicy instead.")
+  ];
+
+  ###### interface
+
+  options = {
+
+    nix = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc ''
+          Whether to enable Nix.
+          Disabling Nix makes the system hard to modify and the Nix programs and configuration will not be made available by NixOS itself.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.nix;
+        defaultText = literalExpression "pkgs.nix";
+        description = lib.mdDoc ''
+          This option specifies the Nix package instance to use throughout the system.
+        '';
+      };
+
+      daemonCPUSchedPolicy = mkOption {
+        type = types.enum [ "other" "batch" "idle" ];
+        default = "other";
+        example = "batch";
+        description = lib.mdDoc ''
+          Nix daemon process CPU scheduling policy. This policy propagates to
+          build processes. `other` is the default scheduling
+          policy for regular tasks. The `batch` policy is
+          similar to `other`, but optimised for
+          non-interactive tasks. `idle` is for extremely
+          low-priority tasks that should only be run when no other task
+          requires CPU time.
+
+          Please note that while using the `idle` policy may
+          greatly improve responsiveness of a system performing expensive
+          builds, it may also slow down and potentially starve crucial
+          configuration updates during load.
+
+          `idle` may therefore be a sensible policy for
+          systems that experience only intermittent phases of high CPU load,
+          such as desktop or portable computers used interactively. Other
+          systems should use the `other` or
+          `batch` policy instead.
+
+          For more fine-grained resource control, please refer to
+          {manpage}`systemd.resource-control(5)` and adjust
+          {option}`systemd.services.nix-daemon` directly.
+      '';
+      };
+
+      daemonIOSchedClass = mkOption {
+        type = types.enum [ "best-effort" "idle" ];
+        default = "best-effort";
+        example = "idle";
+        description = lib.mdDoc ''
+          Nix daemon process I/O scheduling class. This class propagates to
+          build processes. `best-effort` is the default
+          class for regular tasks. The `idle` class is for
+          extremely low-priority tasks that should only perform I/O when no
+          other task does.
+
+          Please note that while using the `idle` scheduling
+          class can improve responsiveness of a system performing expensive
+          builds, it might also slow down or starve crucial configuration
+          updates during load.
+
+          `idle` may therefore be a sensible class for
+          systems that experience only intermittent phases of high I/O load,
+          such as desktop or portable computers used interactively. Other
+          systems should use the `best-effort` class.
+      '';
+      };
+
+      daemonIOSchedPriority = mkOption {
+        type = types.int;
+        default = 4;
+        example = 1;
+        description = lib.mdDoc ''
+          Nix daemon process I/O scheduling priority. This priority propagates
+          to build processes. The supported priorities depend on the
+          scheduling policy: With idle, priorities are not used in scheduling
+          decisions. best-effort supports values in the range 0 (high) to 7
+          (low).
+        '';
+      };
+
+      # Environment variables for running Nix.
+      envVars = mkOption {
+        type = types.attrs;
+        internal = true;
+        default = { };
+        description = lib.mdDoc "Environment variables used by Nix.";
+      };
+
+      nrBuildUsers = mkOption {
+        type = types.int;
+        description = lib.mdDoc ''
+          Number of `nixbld` user accounts created to
+          perform secure concurrent builds.  If you receive an error
+          message saying that “all build users are currently in use”,
+          you should increase this value.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages =
+      [
+        nixPackage
+        pkgs.nix-info
+      ]
+      ++ optional (config.programs.bash.enableCompletion) pkgs.nix-bash-completions;
+
+    systemd.packages = [ nixPackage ];
+
+    systemd.tmpfiles =
+      if (isNixAtLeast "2.8") then {
+        packages = [ nixPackage ];
+      } else {
+        rules = [
+          "d /nix/var/nix/daemon-socket 0755 root root - -"
+        ];
+      };
+
+    systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ];
+
+    systemd.services.nix-daemon =
+      {
+        path = [ nixPackage pkgs.util-linux config.programs.ssh.package ]
+          ++ optionals cfg.distributedBuilds [ pkgs.gzip ];
+
+        environment = cfg.envVars
+          // { CURL_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; }
+          // config.networking.proxy.envVars;
+
+        unitConfig.RequiresMountsFor = "/nix/store";
+
+        serviceConfig =
+          {
+            CPUSchedulingPolicy = cfg.daemonCPUSchedPolicy;
+            IOSchedulingClass = cfg.daemonIOSchedClass;
+            IOSchedulingPriority = cfg.daemonIOSchedPriority;
+            LimitNOFILE = 1048576;
+          };
+
+        restartTriggers = [ config.environment.etc."nix/nix.conf".source ];
+
+        # `stopIfChanged = false` changes to switch behavior
+        # from   stop -> update units -> start
+        #   to   update units -> restart
+        #
+        # The `stopIfChanged` setting therefore controls a trade-off between a
+        # more predictable lifecycle, which runs the correct "version" of
+        # the `ExecStop` line, and on the other hand the availability of
+        # sockets during the switch, as the effectiveness of the stop operation
+        # depends on the socket being stopped as well.
+        #
+        # As `nix-daemon.service` does not make use of `ExecStop`, we prefer
+        # to keep the socket up and available. This is important for machines
+        # that run Nix-based services, such as automated build, test, and deploy
+        # services, that expect the daemon socket to be available at all times.
+        #
+        # Notably, the Nix client does not retry on failure to connect to the
+        # daemon socket, and the in-process RemoteStore instance will disable
+        # itself. This makes retries infeasible even for services that are
+        # aware of the issue. Failure to connect can affect not only new client
+        # processes, but also new RemoteStore instances in existing processes,
+        # as well as existing RemoteStore instances that have not saturated
+        # their connection pool.
+        #
+        # Also note that `stopIfChanged = true` does not kill existing
+        # connection handling daemons, as one might wish to happen before a
+        # breaking Nix upgrade (which is rare). The daemon forks that handle
+        # the individual connections split off into their own sessions, causing
+        # them not to be stopped by systemd.
+        # If a Nix upgrade does require all existing daemon processes to stop,
+        # nix-daemon must do so on its own accord, and only when the new version
+        # starts and detects that Nix's persistent state needs an upgrade.
+        stopIfChanged = false;
+
+      };
+
+    # Set up the environment variables for running Nix.
+    environment.sessionVariables = cfg.envVars;
+
+    nix.nrBuildUsers = mkDefault (
+      if cfg.settings.auto-allocate-uids or false then 0
+      else max 32 (if cfg.settings.max-jobs == "auto" then 0 else cfg.settings.max-jobs)
+    );
+
+    users.users = nixbldUsers;
+
+    services.xserver.displayManager.hiddenUsers = attrNames nixbldUsers;
+
+    system.activationScripts.nix = stringAfter [ "etc" "users" ]
+      ''
+        install -m 0755 -d /nix/var/nix/{gcroots,profiles}/per-user
+      '';
+
+    # Legacy configuration conversion.
+    nix.settings = mkMerge [
+      (mkIf (isNixAtLeast "2.3pre") { sandbox-fallback = false; })
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/system/nscd.nix b/nixos/modules/services/system/nscd.nix
index fdc5190d084..971dffbadc1 100644
--- a/nixos/modules/services/system/nscd.nix
+++ b/nixos/modules/services/system/nscd.nix
@@ -29,10 +29,11 @@ in
 
       enableNsncd = mkOption {
         type = types.bool;
-        default = false;
+        default = true;
         description = lib.mdDoc ''
-          Whether to use nsncd instead of nscd.
+          Whether to use nsncd instead of nscd from glibc.
           This is a nscd-compatible daemon, that proxies lookups, without any caching.
+          Using nscd from glibc is discouraged.
         '';
       };
 
@@ -55,7 +56,10 @@ in
       config = mkOption {
         type = types.lines;
         default = builtins.readFile ./nscd.conf;
-        description = lib.mdDoc "Configuration to use for Name Service Cache Daemon.";
+        description = lib.mdDoc ''
+          Configuration to use for Name Service Cache Daemon.
+          Only used in case glibc-nscd is used.
+        '';
       };
 
       package = mkOption {
diff --git a/nixos/modules/services/system/self-deploy.nix b/nixos/modules/services/system/self-deploy.nix
index 16a793a4225..b5d8ea3f56e 100644
--- a/nixos/modules/services/system/self-deploy.nix
+++ b/nixos/modules/services/system/self-deploy.nix
@@ -125,14 +125,16 @@ in
   };
 
   config = lib.mkIf cfg.enable {
-    systemd.services.self-deploy = {
+    systemd.services.self-deploy = rec {
       inherit (cfg) startAt;
 
-      wantedBy = [ "multi-user.target" ];
+      serviceConfig.Type = "oneshot";
 
       requires = lib.mkIf (!(isPathType cfg.repository)) [ "network-online.target" ];
 
-      environment.GIT_SSH_COMMAND = lib.mkIf (!(isNull cfg.sshKeyFile))
+      after = requires;
+
+      environment.GIT_SSH_COMMAND = lib.mkIf (cfg.sshKeyFile != null)
         "${pkgs.openssh}/bin/ssh -i ${lib.escapeShellArg cfg.sshKeyFile}";
 
       restartIfChanged = false;
diff --git a/nixos/modules/services/torrent/deluge.nix b/nixos/modules/services/torrent/deluge.nix
index de3d077daec..003f7b2613b 100644
--- a/nixos/modules/services/torrent/deluge.nix
+++ b/nixos/modules/services/torrent/deluge.nix
@@ -93,7 +93,7 @@ in {
             `true`.
 
             It does NOT apply to the daemon port nor the web UI port. To access those
-            ports secuerly check the documentation
+            ports securely check the documentation
             <https://dev.deluge-torrent.org/wiki/UserGuide/ThinClient#CreateSSHTunnel>
             or use a VPN or configure certificates for deluge.
           '';
diff --git a/nixos/modules/services/torrent/flexget.nix b/nixos/modules/services/torrent/flexget.nix
index 2a9ffac18d9..1b971838b32 100644
--- a/nixos/modules/services/torrent/flexget.nix
+++ b/nixos/modules/services/torrent/flexget.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg = config.services.flexget;
-  pkg = pkgs.flexget;
+  pkg = cfg.package;
   ymlFile = pkgs.writeText "flexget.yml" ''
     ${cfg.config}
 
@@ -16,6 +16,8 @@ in {
     services.flexget = {
       enable = mkEnableOption (lib.mdDoc "Run FlexGet Daemon");
 
+      package = mkPackageOptionMD pkgs "flexget" {};
+
       user = mkOption {
         default = "deluge";
         example = "some_user";
diff --git a/nixos/modules/services/torrent/magnetico.nix b/nixos/modules/services/torrent/magnetico.nix
index b813f120511..dc6b4e9aa73 100644
--- a/nixos/modules/services/torrent/magnetico.nix
+++ b/nixos/modules/services/torrent/magnetico.nix
@@ -144,7 +144,7 @@ in {
         interface. If unset no authentication will be required.
 
         The file must contain user names and password hashes in the format
-        `username:hash `, one for each line.  Usernames must
+        `username:hash`, one for each line.  Usernames must
         start with a lowecase ([a-z]) ASCII character, might contain
         non-consecutive underscores except at the end, and consists of
         small-case a-z characters and digits 0-9.
diff --git a/nixos/modules/services/torrent/rtorrent.nix b/nixos/modules/services/torrent/rtorrent.nix
index 627439e1079..64cda7fb675 100644
--- a/nixos/modules/services/torrent/rtorrent.nix
+++ b/nixos/modules/services/torrent/rtorrent.nix
@@ -19,6 +19,15 @@ in {
       '';
     };
 
+    dataPermissions = mkOption {
+      type = types.str;
+      default = "0750";
+      example = "0755";
+      description = lib.mdDoc ''
+        Unix Permissions in octal on the rtorrent directory.
+      '';
+    };
+
     downloadDir = mkOption {
       type = types.str;
       default = "${cfg.dataDir}/download";
@@ -205,7 +214,7 @@ in {
         };
       };
 
-      tmpfiles.rules = [ "d '${cfg.dataDir}' 0750 ${cfg.user} ${cfg.group} -" ];
+      tmpfiles.rules = [ "d '${cfg.dataDir}' ${cfg.dataPermissions} ${cfg.user} ${cfg.group} -" ];
     };
   };
 }
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index 43782338483..752ab91fe63 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -19,8 +19,8 @@ in
   imports = [
     (mkRenamedOptionModule ["services" "transmission" "port"]
                            ["services" "transmission" "settings" "rpc-port"])
-    (mkAliasOptionModule ["services" "transmission" "openFirewall"]
-                         ["services" "transmission" "openPeerPorts"])
+    (mkAliasOptionModuleMD ["services" "transmission" "openFirewall"]
+                           ["services" "transmission" "openPeerPorts"])
   ];
   options = {
     services.transmission = {
@@ -174,7 +174,7 @@ in
         };
       };
 
-      package = mkPackageOption pkgs "transmission" {};
+      package = mkPackageOptionMD pkgs "transmission" {};
 
       downloadDirPermissions = mkOption {
         type = with types; nullOr str;
diff --git a/nixos/modules/services/ttys/kmscon.nix b/nixos/modules/services/ttys/kmscon.nix
index f5a8d8b104d..1d98e7ddacc 100644
--- a/nixos/modules/services/ttys/kmscon.nix
+++ b/nixos/modules/services/ttys/kmscon.nix
@@ -99,6 +99,7 @@ in {
     systemd.units."kmsconvt@.service".aliases = [ "autovt@.service" ];
 
     systemd.services.systemd-vconsole-setup.enable = false;
+    systemd.services.reload-systemd-vconsole-setup.enable = false;
 
     services.kmscon.extraConfig =
       let
diff --git a/nixos/modules/services/video/epgstation/default.nix b/nixos/modules/services/video/epgstation/default.nix
index 3d1d7a27c21..fca483b0dbd 100644
--- a/nixos/modules/services/video/epgstation/default.nix
+++ b/nixos/modules/services/video/epgstation/default.nix
@@ -80,11 +80,11 @@ in
   options.services.epgstation = {
     enable = lib.mkEnableOption (lib.mdDoc description);
 
-    package = lib.mkOption {
-      default = pkgs.epgstation;
-      type = lib.types.package;
-      defaultText = lib.literalExpression "pkgs.epgstation";
-      description = lib.mdDoc "epgstation package to use";
+    package = lib.mkPackageOptionMD pkgs "epgstation" { };
+
+    ffmpeg = lib.mkPackageOptionMD pkgs "ffmpeg" {
+      default = [ "ffmpeg-headless" ];
+      example = "pkgs.ffmpeg-full";
     };
 
     usePreconfiguredStreaming = lib.mkOption {
@@ -264,6 +264,9 @@ in
       description = "EPGStation user";
       group = config.users.groups.epgstation.name;
       isSystemUser = true;
+
+      # NPM insists on creating ~/.npm
+      home = "/var/cache/epgstation";
     };
 
     users.groups.epgstation = { };
@@ -275,6 +278,8 @@ in
       package = lib.mkDefault pkgs.mariadb;
       ensureDatabases = [ cfg.database.name ];
       # FIXME: enable once mysqljs supports auth_socket
+      # https://github.com/mysqljs/mysql/issues/1507
+      #
       # ensureUsers = [ {
       #   name = username;
       #   ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
@@ -292,8 +297,8 @@ in
             database = cfg.database.name;
           };
 
-          ffmpeg = lib.mkDefault "${pkgs.ffmpeg-full}/bin/ffmpeg";
-          ffprobe = lib.mkDefault "${pkgs.ffmpeg-full}/bin/ffprobe";
+          ffmpeg = lib.mkDefault "${cfg.ffmpeg}/bin/ffmpeg";
+          ffprobe = lib.mkDefault "${cfg.ffmpeg}/bin/ffprobe";
 
           # for disambiguation with TypeScript files
           recordedFileExtension = lib.mkDefault ".m2ts";
@@ -305,9 +310,15 @@ in
       ];
 
     systemd.tmpfiles.rules = [
+      "d '/var/lib/epgstation/key' - ${username} ${groupname} - -"
       "d '/var/lib/epgstation/streamfiles' - ${username} ${groupname} - -"
+      "d '/var/lib/epgstation/drop' - ${username} ${groupname} - -"
       "d '/var/lib/epgstation/recorded' - ${username} ${groupname} - -"
       "d '/var/lib/epgstation/thumbnail' - ${username} ${groupname} - -"
+      "d '/var/lib/epgstation/db/subscribers' - ${username} ${groupname} - -"
+      "d '/var/lib/epgstation/db/migrations/mysql' - ${username} ${groupname} - -"
+      "d '/var/lib/epgstation/db/migrations/postgres' - ${username} ${groupname} - -"
+      "d '/var/lib/epgstation/db/migrations/sqlite' - ${username} ${groupname} - -"
     ];
 
     systemd.services.epgstation = {
@@ -318,11 +329,14 @@ in
         ++ lib.optional config.services.mirakurun.enable "mirakurun.service"
         ++ lib.optional config.services.mysql.enable "mysql.service";
 
+      environment.NODE_ENV = "production";
+
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/epgstation start";
         ExecStartPre = "+${preStartScript}";
         User = username;
         Group = groupname;
+        CacheDirectory = "epgstation";
         StateDirectory = "epgstation";
         LogsDirectory = "epgstation";
         ConfigurationDirectory = "epgstation";
diff --git a/nixos/modules/services/video/frigate.nix b/nixos/modules/services/video/frigate.nix
new file mode 100644
index 00000000000..217637cbebc
--- /dev/null
+++ b/nixos/modules/services/video/frigate.nix
@@ -0,0 +1,368 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  inherit (lib)
+    literalExpression
+    mkDefault
+    mdDoc
+    mkEnableOption
+    mkIf
+    mkOption
+    types;
+
+  cfg = config.services.frigate;
+
+  format = pkgs.formats.yaml {};
+
+  filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! lib.elem v [ null ])) cfg.settings;
+
+  cameraFormat = with types; submodule {
+    freeformType = format.type;
+    options = {
+      ffmpeg = {
+        inputs = mkOption {
+          description = mdDoc ''
+            List of inputs for this camera.
+          '';
+          type = listOf (submodule {
+            freeformType = format.type;
+            options = {
+              path = mkOption {
+                type = str;
+                example = "rtsp://192.0.2.1:554/rtsp";
+                description = mdDoc ''
+                  Stream URL
+                '';
+              };
+              roles = mkOption {
+                type = listOf (enum [ "detect" "record" "rtmp" ]);
+                example = literalExpression ''
+                  [ "detect" "rtmp" ]
+                '';
+                description = mdDoc ''
+                  List of roles for this stream
+                '';
+              };
+            };
+          });
+        };
+      };
+    };
+  };
+
+in
+
+{
+  meta.buildDocsInSandbox = false;
+
+  options.services.frigate = with types; {
+    enable = mkEnableOption (mdDoc "Frigate NVR");
+
+    package = mkOption {
+      type = package;
+      default = pkgs.frigate;
+      description = mdDoc ''
+        The frigate package to use.
+      '';
+    };
+
+    hostname = mkOption {
+      type = str;
+      example = "frigate.exampe.com";
+      description = mdDoc ''
+        Hostname of the nginx vhost to configure.
+
+        Only nginx is supported by upstream for direct reverse proxying.
+      '';
+    };
+
+    settings = mkOption {
+      type = submodule {
+        freeformType = format.type;
+        options = {
+          cameras = mkOption {
+            type = attrsOf cameraFormat;
+            description = mdDoc ''
+              Attribute set of cameras configurations.
+
+              https://docs.frigate.video/configuration/cameras
+            '';
+          };
+
+          database = {
+            path = mkOption {
+              type = path;
+              default = "/var/lib/frigate/frigate.db";
+              description = mdDoc ''
+                Path to the SQLite database used
+              '';
+            };
+          };
+
+          mqtt = {
+            enabled = mkEnableOption (mdDoc "MQTT support");
+
+            host = mkOption {
+              type = nullOr str;
+              default = null;
+              example = "mqtt.example.com";
+              description = mdDoc ''
+                MQTT server hostname
+              '';
+            };
+          };
+        };
+      };
+      default = {};
+      description = mdDoc ''
+        Frigate configuration as a nix attribute set.
+
+        See the project documentation for how to configure frigate.
+        - [Creating a config file](https://docs.frigate.video/guides/getting_started)
+        - [Configuration reference](https://docs.frigate.video/configuration/index)
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.nginx = {
+      enable =true;
+      additionalModules = with pkgs.nginxModules; [
+        secure-token
+        rtmp
+        vod
+      ];
+      recommendedProxySettings = mkDefault true;
+      recommendedGzipSettings = mkDefault true;
+      upstreams = {
+        frigate-api.servers = {
+          "127.0.0.1:5001" = {};
+        };
+        frigate-mqtt-ws.servers = {
+          "127.0.0.1:5002" = {};
+        };
+        frigate-jsmpeg.servers = {
+          "127.0.0.1:8082" = {};
+        };
+        frigate-go2rtc.servers = {
+          "127.0.0.1:1984" = {};
+        };
+      };
+      # Based on https://github.com/blakeblackshear/frigate/blob/v0.12.0/docker/rootfs/usr/local/nginx/conf/nginx.conf
+      virtualHosts."${cfg.hostname}" = {
+        locations = {
+          "/api/" = {
+            proxyPass = "http://frigate-api/";
+          };
+          "~* /api/.*\.(jpg|jpeg|png)$" = {
+            proxyPass = "http://frigate-api";
+            extraConfig = ''
+              add_header 'Access-Control-Allow-Origin' '*';
+              add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
+              rewrite ^/api/(.*)$ $1 break;
+            '';
+          };
+          "/vod/" = {
+            extraConfig = ''
+              aio threads;
+              vod hls;
+
+              secure_token $args;
+              secure_token_types application/vnd.apple.mpegurl;
+
+              add_header Access-Control-Allow-Headers '*';
+              add_header Access-Control-Expose-Headers 'Server,range,Content-Length,Content-Range';
+              add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS';
+              add_header Access-Control-Allow-Origin '*';
+              add_header Cache-Control "no-store";
+              expires off;
+            '';
+          };
+          "/stream/" = {
+            # TODO
+          };
+          "/ws" = {
+            proxyPass = "http://frigate-mqtt-ws/";
+            proxyWebsockets = true;
+          };
+          "/live/jsmpeg" = {
+            proxyPass = "http://frigate-jsmpeg/";
+            proxyWebsockets = true;
+          };
+          "/live/mse/" = {
+            proxyPass = "http://frigate-go2rtc/";
+            proxyWebsockets = true;
+          };
+          "/live/webrtc/" = {
+            proxyPass = "http://frigate-go2rtc/";
+            proxyWebsockets = true;
+          };
+          "/cache/" = {
+            alias = "/var/cache/frigate/";
+          };
+          "/clips/" = {
+            root = "/var/lib/frigate";
+            extraConfig = ''
+              add_header 'Access-Control-Allow-Origin' "$http_origin" always;
+              add_header 'Access-Control-Allow-Credentials' 'true';
+              add_header 'Access-Control-Expose-Headers' 'Content-Length';
+              if ($request_method = 'OPTIONS') {
+                  add_header 'Access-Control-Allow-Origin' "$http_origin";
+                  add_header 'Access-Control-Max-Age' 1728000;
+                  add_header 'Content-Type' 'text/plain charset=UTF-8';
+                  add_header 'Content-Length' 0;
+                  return 204;
+              }
+
+              types {
+                  video/mp4 mp4;
+                  image/jpeg jpg;
+              }
+
+              autoindex on;
+            '';
+          };
+          "/recordings/" = {
+            root = "/var/lib/frigate";
+            extraConfig = ''
+              add_header 'Access-Control-Allow-Origin' "$http_origin" always;
+              add_header 'Access-Control-Allow-Credentials' 'true';
+              add_header 'Access-Control-Expose-Headers' 'Content-Length';
+              if ($request_method = 'OPTIONS') {
+                  add_header 'Access-Control-Allow-Origin' "$http_origin";
+                  add_header 'Access-Control-Max-Age' 1728000;
+                  add_header 'Content-Type' 'text/plain charset=UTF-8';
+                  add_header 'Content-Length' 0;
+                  return 204;
+              }
+
+              types {
+                  video/mp4 mp4;
+              }
+
+              autoindex on;
+              autoindex_format json;
+            '';
+          };
+          "/assets/" = {
+            root = cfg.package.web;
+            extraConfig = ''
+              access_log off;
+              expires 1y;
+              add_header Cache-Control "public";
+            '';
+          };
+          "/" = {
+            root = cfg.package.web;
+            tryFiles = "$uri $uri/ /index.html";
+            extraConfig = ''
+              add_header Cache-Control "no-store";
+              expires off;
+
+              sub_filter 'href="/BASE_PATH/' 'href="$http_x_ingress_path/';
+              sub_filter 'url(/BASE_PATH/' 'url($http_x_ingress_path/';
+              sub_filter '"/BASE_PATH/dist/' '"$http_x_ingress_path/dist/';
+              sub_filter '"/BASE_PATH/js/' '"$http_x_ingress_path/js/';
+              sub_filter '"/BASE_PATH/assets/' '"$http_x_ingress_path/assets/';
+              sub_filter '"/BASE_PATH/monacoeditorwork/' '"$http_x_ingress_path/assets/';
+              sub_filter 'return"/BASE_PATH/"' 'return window.baseUrl';
+              sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path/";</script>';
+              sub_filter_types text/css application/javascript;
+              sub_filter_once off;
+            '';
+          };
+        };
+        extraConfig = ''
+          # vod settings
+          vod_base_url "";
+          vod_segments_base_url "";
+          vod_mode mapped;
+          vod_max_mapping_response_size 1m;
+          vod_upstream_location /api;
+          vod_align_segments_to_key_frames on;
+          vod_manifest_segment_durations_mode accurate;
+          vod_ignore_edit_list on;
+          vod_segment_duration 10000;
+          vod_hls_mpegts_align_frames off;
+          vod_hls_mpegts_interleave_frames on;
+          # file handle caching / aio
+          open_file_cache max=1000 inactive=5m;
+          open_file_cache_valid 2m;
+          open_file_cache_min_uses 1;
+          open_file_cache_errors on;
+          aio on;
+          # https://github.com/kaltura/nginx-vod-module#vod_open_file_thread_pool
+          vod_open_file_thread_pool default;
+          # vod caches
+          vod_metadata_cache metadata_cache 512m;
+          vod_mapping_cache mapping_cache 5m 10m;
+          # gzip manifest
+          gzip_types application/vnd.apple.mpegurl;
+        '';
+      };
+      appendConfig = ''
+        rtmp {
+            server {
+                listen 1935;
+                chunk_size 4096;
+                allow publish 127.0.0.1;
+                deny publish all;
+                allow play all;
+                application live {
+                    live on;
+                    record off;
+                    meta copy;
+                }
+            }
+        }
+      '';
+    };
+
+    systemd.services.frigate = {
+      after = [
+        "go2rtc.service"
+        "network.target"
+      ];
+      wantedBy = [
+        "multi-user.target"
+      ];
+      environment = {
+        CONFIG_FILE = format.generate "frigate.yml" filteredConfig;
+        HOME = "/var/lib/frigate";
+        PYTHONPATH = cfg.package.pythonPath;
+      };
+      path = with pkgs; [
+        # unfree:
+        # config.boot.kernelPackages.nvidiaPackages.latest.bin
+        ffmpeg_5-headless
+        libva-utils
+        procps
+        radeontop
+      ] ++ lib.optionals (!stdenv.isAarch64) [
+        # not available on aarch64-linux
+        intel-gpu-tools
+      ];
+      serviceConfig = {
+        ExecStart = "${cfg.package.python.interpreter} -m frigate";
+
+        DynamicUser = true;
+        User = "frigate";
+
+        StateDirectory = "frigate";
+        UMask = "0077";
+
+        # Caches
+        PrivateTmp = true;
+        CacheDirectory = "frigate";
+
+        BindPaths = [
+          "/migrations:${cfg.package}/share/frigate/migrations:ro"
+        ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/video/go2rtc/default.nix b/nixos/modules/services/video/go2rtc/default.nix
new file mode 100644
index 00000000000..1151d31b68e
--- /dev/null
+++ b/nixos/modules/services/video/go2rtc/default.nix
@@ -0,0 +1,115 @@
+{ lib
+, config
+, options
+, pkgs
+, ...
+}:
+
+let
+  inherit (lib)
+    literalExpression
+    mdDoc
+    mkEnableOption
+    mkOption
+    mkPackageOptionMD
+    types
+    ;
+
+  cfg = config.services.go2rtc;
+  opt = options.services.go2rtc;
+
+  format = pkgs.formats.yaml {};
+  configFile = format.generate "go2rtc.yaml" cfg.settings;
+in
+
+{
+  meta.buildDocsInSandbox = false;
+
+  options.services.go2rtc = with types; {
+    enable = mkEnableOption (mdDoc "go2rtc streaming server");
+
+    package = mkPackageOptionMD pkgs "go2rtc" { };
+
+    settings = mkOption {
+      default = {};
+      description = mdDoc ''
+        go2rtc configuration as a Nix attribute set.
+
+        See the [wiki](https://github.com/AlexxIT/go2rtc/wiki/Configuration) for possible configuration options.
+      '';
+      type = submodule {
+        freeformType = format.type;
+        options = {
+          # https://github.com/AlexxIT/go2rtc/blob/v1.5.0/README.md#module-api
+          api = {
+            listen = mkOption {
+              type = str;
+              default = ":1984";
+              example = "127.0.0.1:1984";
+              description = mdDoc ''
+                API listen address, conforming to a Go address string.
+              '';
+            };
+          };
+
+          # https://github.com/AlexxIT/go2rtc/blob/v1.5.0/README.md#source-ffmpeg
+          ffmpeg = {
+            bin = mkOption {
+              type = path;
+              default = "${lib.getBin pkgs.ffmpeg_6-headless}/bin/ffmpeg";
+              defaultText = literalExpression "\${lib.getBin pkgs.ffmpeg_6-headless}/bin/ffmpeg";
+              description = mdDoc ''
+                The ffmpeg package to use for transcoding.
+              '';
+            };
+          };
+
+          # TODO: https://github.com/AlexxIT/go2rtc/blob/v1.5.0/README.md#module-rtsp
+          rtsp = {
+          };
+
+          streams = mkOption {
+            type = attrsOf (either str (listOf str));
+            default = {};
+            example = literalExpression ''
+              {
+                cam1 = "onvif://admin:password@192.168.1.123:2020";
+                cam2 = "tcp://192.168.1.123:12345";
+              }
+            '';
+            description = mdDoc ''
+              Stream source configuration. Multiple source types are supported.
+
+              Check the [configuration reference](https://github.com/AlexxIT/go2rtc/blob/v${cfg.package.version}/README.md#module-streams) for possible options.
+            '';
+          };
+
+          # TODO: https://github.com/AlexxIT/go2rtc/blob/v1.5.0/README.md#module-webrtc
+          webrtc = {
+          };
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.go2rtc = {
+      after = [
+        "network-online.target"
+      ];
+      wantedBy = [
+        "multi-user.target"
+      ];
+      serviceConfig = {
+        DynamicUser = true;
+        User = "go2rtc";
+        SupplementaryGroups = [
+          # for v4l2 devices
+          "video"
+        ];
+        StateDirectory = "go2rtc";
+        ExecStart = "${cfg.package}/bin/go2rtc -config ${configFile}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/video/rtsp-simple-server.nix b/nixos/modules/services/video/mediamtx.nix
index 2dd62edab78..18a9e3d5fe3 100644
--- a/nixos/modules/services/video/rtsp-simple-server.nix
+++ b/nixos/modules/services/video/mediamtx.nix
@@ -3,19 +3,19 @@
 with lib;
 
 let
-  cfg = config.services.rtsp-simple-server;
-  package = pkgs.rtsp-simple-server;
+  cfg = config.services.mediamtx;
+  package = pkgs.mediamtx;
   format = pkgs.formats.yaml {};
 in
 {
   options = {
-    services.rtsp-simple-server = {
-      enable = mkEnableOption (lib.mdDoc "RTSP Simple Server");
+    services.mediamtx = {
+      enable = mkEnableOption (lib.mdDoc "MediaMTX");
 
       settings = mkOption {
         description = lib.mdDoc ''
-          Settings for rtsp-simple-server.
-          Read more at <https://github.com/aler9/rtsp-simple-server/blob/main/rtsp-simple-server.yml>
+          Settings for MediaMTX.
+          Read more at <https://github.com/aler9/mediamtx/blob/main/mediamtx.yml>
         '';
         type = format.type;
 
@@ -25,7 +25,7 @@ in
             "stdout"
           ];
           # we set this so when the user uses it, it just works (see LogsDirectory below). but it's not used by default.
-          logFile = "/var/log/rtsp-simple-server/rtsp-simple-server.log";
+          logFile = "/var/log/mediamtx/mediamtx.log";
         };
 
         example = {
@@ -40,20 +40,20 @@ in
 
       env = mkOption {
         type = with types; attrsOf anything;
-        description = lib.mdDoc "Extra environment variables for RTSP Simple Server";
+        description = lib.mdDoc "Extra environment variables for MediaMTX";
         default = {};
         example = {
-          RTSP_CONFKEY = "mykey";
+          MTX_CONFKEY = "mykey";
         };
       };
     };
   };
 
   config = mkIf (cfg.enable) {
-    # NOTE: rtsp-simple-server watches this file and automatically reloads if it changes
-    environment.etc."rtsp-simple-server.yaml".source = format.generate "rtsp-simple-server.yaml" cfg.settings;
+    # NOTE: mediamtx watches this file and automatically reloads if it changes
+    environment.etc."mediamtx.yaml".source = format.generate "mediamtx.yaml" cfg.settings;
 
-    systemd.services.rtsp-simple-server = {
+    systemd.services.mediamtx = {
       environment = cfg.env;
 
       after = [ "network.target" ];
@@ -65,15 +65,15 @@ in
 
       serviceConfig = {
         DynamicUser = true;
-        User = "rtsp-simple-server";
-        Group = "rtsp-simple-server";
+        User = "mediamtx";
+        Group = "mediamtx";
 
-        LogsDirectory = "rtsp-simple-server";
+        LogsDirectory = "mediamtx";
 
         # user likely may want to stream cameras, can't hurt to add video group
         SupplementaryGroups = "video";
 
-        ExecStart = "${package}/bin/rtsp-simple-server /etc/rtsp-simple-server.yaml";
+        ExecStart = "${package}/bin/mediamtx /etc/mediamtx.yaml";
       };
     };
   };
diff --git a/nixos/modules/services/video/mirakurun.nix b/nixos/modules/services/video/mirakurun.nix
index 5484515e7cb..31f90650ba9 100644
--- a/nixos/modules/services/video/mirakurun.nix
+++ b/nixos/modules/services/video/mirakurun.nix
@@ -154,6 +154,9 @@ in
         description = "Mirakurun user";
         group = "video";
         isSystemUser = true;
+
+        # NPM insists on creating ~/.npm
+        home = "/var/cache/mirakurun";
       };
 
       services.mirakurun.serverSettings = {
@@ -171,9 +174,10 @@ in
         wantedBy = [ "multi-user.target" ];
         after = [ "network.target" ];
         serviceConfig = {
-          ExecStart = "${mirakurun}/bin/mirakurun-start";
+          ExecStart = "${mirakurun}/bin/mirakurun start";
           User = username;
           Group = groupname;
+          CacheDirectory = "mirakurun";
           RuntimeDirectory="mirakurun";
           StateDirectory="mirakurun";
           Nice = -10;
diff --git a/nixos/modules/services/video/unifi-video.nix b/nixos/modules/services/video/unifi-video.nix
index 93d4d5060b7..cb438a08150 100644
--- a/nixos/modules/services/video/unifi-video.nix
+++ b/nixos/modules/services/video/unifi-video.nix
@@ -123,7 +123,7 @@ in
 
     mongodbPackage = mkOption {
       type = types.package;
-      default = pkgs.mongodb-4_0;
+      default = pkgs.mongodb-4_4;
       defaultText = literalExpression "pkgs.mongodb";
       description = lib.mdDoc ''
         The mongodb package to use.
diff --git a/nixos/modules/services/video/v4l2-relayd.nix b/nixos/modules/services/video/v4l2-relayd.nix
new file mode 100644
index 00000000000..2a9dbe00158
--- /dev/null
+++ b/nixos/modules/services/video/v4l2-relayd.nix
@@ -0,0 +1,199 @@
+{ config, lib, pkgs, utils, ... }:
+let
+
+  inherit (lib) attrValues concatStringsSep filterAttrs length listToAttrs literalExpression
+    makeSearchPathOutput mkEnableOption mkIf mkOption nameValuePair optionals types;
+  inherit (utils) escapeSystemdPath;
+
+  cfg = config.services.v4l2-relayd;
+
+  kernelPackages = config.boot.kernelPackages;
+
+  gst = (with pkgs.gst_all_1; [
+    gst-plugins-bad
+    gst-plugins-base
+    gst-plugins-good
+    gstreamer.out
+  ]);
+
+  instanceOpts = { name, ... }: {
+    options = {
+      enable = mkEnableOption (lib.mdDoc "this v4l2-relayd instance");
+
+      name = mkOption {
+        type = types.str;
+        default = name;
+        description = lib.mdDoc ''
+          The name of the instance.
+        '';
+      };
+
+      cardLabel = mkOption {
+        type = types.str;
+        description = lib.mdDoc ''
+          The name the camera will show up as.
+        '';
+      };
+
+      extraPackages = mkOption {
+        type = with types; listOf package;
+        default = [ ];
+        description = lib.mdDoc ''
+          Extra packages to add to {env}`GST_PLUGIN_PATH` for the instance.
+        '';
+      };
+
+      input = {
+        pipeline = mkOption {
+          type = types.str;
+          description = lib.mdDoc ''
+            The gstreamer-pipeline to use for the input-stream.
+          '';
+        };
+
+        format = mkOption {
+          type = types.str;
+          default = "YUY2";
+          description = lib.mdDoc ''
+            The video-format to read from input-stream.
+          '';
+        };
+
+        width = mkOption {
+          type = types.ints.positive;
+          default = 1280;
+          description = lib.mdDoc ''
+            The width to read from input-stream.
+          '';
+        };
+
+        height = mkOption {
+          type = types.ints.positive;
+          default = 720;
+          description = lib.mdDoc ''
+            The height to read from input-stream.
+          '';
+        };
+
+        framerate = mkOption {
+          type = types.ints.positive;
+          default = 30;
+          description = lib.mdDoc ''
+            The framerate to read from input-stream.
+          '';
+        };
+      };
+
+      output = {
+        format = mkOption {
+          type = types.str;
+          default = "YUY2";
+          description = lib.mdDoc ''
+            The video-format to write to output-stream.
+          '';
+        };
+      };
+
+    };
+  };
+
+in
+{
+
+  options.services.v4l2-relayd = {
+
+    instances = mkOption {
+      type = with types; attrsOf (submodule instanceOpts);
+      default = { };
+      example = literalExpression ''
+        {
+          example = {
+            cardLabel = "Example card";
+            input.pipeline = "videotestsrc";
+          };
+        }
+      '';
+      description = lib.mdDoc ''
+        v4l2-relayd instances to be created.
+      '';
+    };
+
+  };
+
+  config =
+    let
+
+      mkInstanceService = instance: {
+        description = "Streaming relay for v4l2loopback using GStreamer";
+
+        after = [ "modprobe@v4l2loopback.service" "systemd-logind.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          Type = "simple";
+          Restart = "always";
+          PrivateNetwork = true;
+          PrivateTmp = true;
+          LimitNPROC = 1;
+        };
+
+        environment = {
+          GST_PLUGIN_PATH = makeSearchPathOutput "lib" "lib/gstreamer-1.0" (gst ++ instance.extraPackages);
+          V4L2_DEVICE_FILE = "/run/v4l2-relayd-${instance.name}/device";
+        };
+
+        script =
+          let
+            appsrcOptions = concatStringsSep "," [
+              "caps=video/x-raw"
+              "format=${instance.input.format}"
+              "width=${toString instance.input.width}"
+              "height=${toString instance.input.height}"
+              "framerate=${toString instance.input.framerate}/1"
+            ];
+
+            outputPipeline = [
+              "appsrc name=appsrc ${appsrcOptions}"
+              "videoconvert"
+            ] ++ optionals (instance.input.format != instance.output.format) [
+              "video/x-raw,format=${instance.output.format}"
+              "queue"
+            ] ++ [ "v4l2sink name=v4l2sink device=$(cat $V4L2_DEVICE_FILE)" ];
+          in
+          ''
+            exec ${pkgs.v4l2-relayd}/bin/v4l2-relayd -i "${instance.input.pipeline}" -o "${concatStringsSep " ! " outputPipeline}"
+          '';
+
+        preStart = ''
+          mkdir -p $(dirname $V4L2_DEVICE_FILE)
+          ${kernelPackages.v4l2loopback.bin}/bin/v4l2loopback-ctl add -x 1 -n "${instance.cardLabel}" > $V4L2_DEVICE_FILE
+        '';
+
+        postStop = ''
+          ${kernelPackages.v4l2loopback.bin}/bin/v4l2loopback-ctl delete $(cat $V4L2_DEVICE_FILE)
+          rm -rf $(dirname $V4L2_DEVICE_FILE)
+        '';
+      };
+
+      mkInstanceServices = instances: listToAttrs (map
+        (instance:
+          nameValuePair "v4l2-relayd-${escapeSystemdPath instance.name}" (mkInstanceService instance)
+        )
+        instances);
+
+      enabledInstances = attrValues (filterAttrs (n: v: v.enable) cfg.instances);
+
+    in
+    {
+
+      boot = mkIf ((length enabledInstances) > 0) {
+        extraModulePackages = [ kernelPackages.v4l2loopback ];
+        kernelModules = [ "v4l2loopback" ];
+      };
+
+      systemd.services = mkInstanceServices enabledInstances;
+
+    };
+
+  meta.maintainers = with lib.maintainers; [ betaboon ];
+}
diff --git a/nixos/modules/services/web-apps/akkoma.md b/nixos/modules/services/web-apps/akkoma.md
index fc849be0c87..83dd1a8b35f 100644
--- a/nixos/modules/services/web-apps/akkoma.md
+++ b/nixos/modules/services/web-apps/akkoma.md
@@ -152,7 +152,7 @@ services.akkoma.config.":pleroma".":media_preview_proxy" = {
 
 ## Frontend management {#modules-services-akkoma-frontend-management}
 
-Akkoma will be deployed with the `pleroma-fe` and `admin-fe` frontends by default. These can be
+Akkoma will be deployed with the `akkoma-fe` and `admin-fe` frontends by default. These can be
 modified by setting
 [{option}`services.akkoma.frontends`](options.html#opt-services.akkoma.frontends).
 
@@ -160,7 +160,7 @@ The following example overrides the primary frontend’s default configuration u
 derivation.
 
 ```nix
-services.akkoma.frontends.primary.package = pkgs.runCommand "pleroma-fe" {
+services.akkoma.frontends.primary.package = pkgs.runCommand "akkoma-fe" {
   config = builtins.toJSON {
     expertLevel = 1;
     collapseMessageWithSubject = false;
@@ -177,10 +177,10 @@ services.akkoma.frontends.primary.package = pkgs.runCommand "pleroma-fe" {
   passAsFile = [ "config" ];
 } ''
   mkdir $out
-  lndir ${pkgs.akkoma-frontends.pleroma-fe} $out
+  lndir ${pkgs.akkoma-frontends.akkoma-fe} $out
 
   rm $out/static/config.json
-  jq -s add ${pkgs.akkoma-frontends.pleroma-fe}/static/config.json ${config} \
+  jq -s add ${pkgs.akkoma-frontends.akkoma-fe}/static/config.json ${config} \
     >$out/static/config.json
 '';
 ```
@@ -318,8 +318,8 @@ to make packages available in the chroot.
 {option}`services.systemd.akkoma.serviceConfig.BindPaths` and
 {option}`services.systemd.akkoma.serviceConfig.BindReadOnlyPaths` permit access to outside paths
 through bind mounts. Refer to
-[{manpage}`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths=)
-for details.
+[`BindPaths=`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths=)
+of {manpage}`systemd.exec(5)` for details.
 
 ### Distributed deployment {#modules-services-akkoma-distributed-deployment}
 
diff --git a/nixos/modules/services/web-apps/akkoma.nix b/nixos/modules/services/web-apps/akkoma.nix
index 47ba53e4222..8d177525861 100644
--- a/nixos/modules/services/web-apps/akkoma.nix
+++ b/nixos/modules/services/web-apps/akkoma.nix
@@ -51,13 +51,13 @@ let
       package = mkOption {
         type = types.package;
         description = mdDoc "Akkoma frontend package.";
-        example = literalExpression "pkgs.akkoma-frontends.pleroma-fe";
+        example = literalExpression "pkgs.akkoma-frontends.akkoma-fe";
       };
 
       name = mkOption {
         type = types.nonEmptyStr;
         description = mdDoc "Akkoma frontend name.";
-        example = "pleroma-fe";
+        example = "akkoma-fe";
       };
 
       ref = mkOption {
@@ -476,8 +476,8 @@ in {
         type = with types; attrsOf (submodule frontend);
         default = {
           primary = {
-            package = pkgs.akkoma-frontends.pleroma-fe;
-            name = "pleroma-fe";
+            package = pkgs.akkoma-frontends.akkoma-fe;
+            name = "akkoma-fe";
             ref = "stable";
           };
           admin = {
@@ -489,8 +489,8 @@ in {
         defaultText = literalExpression ''
           {
             primary = {
-              package = pkgs.akkoma-frontends.pleroma-fe;
-              name = "pleroma-fe";
+              package = pkgs.akkoma-frontends.akkoma-fe;
+              name = "akkoma-fe";
               ref = "stable";
             };
             admin = {
@@ -1082,5 +1082,5 @@ in {
   };
 
   meta.maintainers = with maintainers; [ mvs ];
-  meta.doc = ./akkoma.xml;
+  meta.doc = ./akkoma.md;
 }
diff --git a/nixos/modules/services/web-apps/akkoma.xml b/nixos/modules/services/web-apps/akkoma.xml
deleted file mode 100644
index 76e6b806f30..00000000000
--- a/nixos/modules/services/web-apps/akkoma.xml
+++ /dev/null
@@ -1,396 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-akkoma">
-  <title>Akkoma</title>
-  <para>
-    <link xlink:href="https://akkoma.dev/">Akkoma</link> is a
-    lightweight ActivityPub microblogging server forked from Pleroma.
-  </para>
-  <section xml:id="modules-services-akkoma-service-configuration">
-    <title>Service configuration</title>
-    <para>
-      The Elixir configuration file required by Akkoma is generated
-      automatically from
-      <link xlink:href="options.html#opt-services.akkoma.config"><option>services.akkoma.config</option></link>.
-      Secrets must be included from external files outside of the Nix
-      store by setting the configuration option to an attribute set
-      containing the attribute <option>_secret</option> – a string
-      pointing to the file containing the actual value of the option.
-    </para>
-    <para>
-      For the mandatory configuration settings these secrets will be
-      generated automatically if the referenced file does not exist
-      during startup, unless disabled through
-      <link xlink:href="options.html#opt-services.akkoma.initSecrets"><option>services.akkoma.initSecrets</option></link>.
-    </para>
-    <para>
-      The following configuration binds Akkoma to the Unix socket
-      <literal>/run/akkoma/socket</literal>, expecting to be run behind
-      a HTTP proxy on <literal>fediverse.example.com</literal>.
-    </para>
-    <programlisting language="nix">
-services.akkoma.enable = true;
-services.akkoma.config = {
-  &quot;:pleroma&quot; = {
-    &quot;:instance&quot; = {
-      name = &quot;My Akkoma instance&quot;;
-      description = &quot;More detailed description&quot;;
-      email = &quot;admin@example.com&quot;;
-      registration_open = false;
-    };
-
-    &quot;Pleroma.Web.Endpoint&quot; = {
-      url.host = &quot;fediverse.example.com&quot;;
-    };
-  };
-};
-</programlisting>
-    <para>
-      Please refer to the
-      <link xlink:href="https://docs.akkoma.dev/stable/configuration/cheatsheet/">configuration
-      cheat sheet</link> for additional configuration options.
-    </para>
-  </section>
-  <section xml:id="modules-services-akkoma-user-management">
-    <title>User management</title>
-    <para>
-      After the Akkoma service is running, the administration utility
-      can be used to
-      <link xlink:href="https://docs.akkoma.dev/stable/administration/CLI_tasks/user/">manage
-      users</link>. In particular an administrative user can be created
-      with
-    </para>
-    <programlisting>
-$ pleroma_ctl user new &lt;nickname&gt; &lt;email&gt; --admin --moderator --password &lt;password&gt;
-</programlisting>
-  </section>
-  <section xml:id="modules-services-akkoma-proxy-configuration">
-    <title>Proxy configuration</title>
-    <para>
-      Although it is possible to expose Akkoma directly, it is common
-      practice to operate it behind an HTTP reverse proxy such as nginx.
-    </para>
-    <programlisting language="nix">
-services.akkoma.nginx = {
-  enableACME = true;
-  forceSSL = true;
-};
-
-services.nginx = {
-  enable = true;
-
-  clientMaxBodySize = &quot;16m&quot;;
-  recommendedTlsSettings = true;
-  recommendedOptimisation = true;
-  recommendedGzipSettings = true;
-};
-</programlisting>
-    <para>
-      Please refer to <xref linkend="module-security-acme" /> for
-      details on how to provision an SSL/TLS certificate.
-    </para>
-    <section xml:id="modules-services-akkoma-media-proxy">
-      <title>Media proxy</title>
-      <para>
-        Without the media proxy function, Akkoma does not store any
-        remote media like pictures or video locally, and clients have to
-        fetch them directly from the source server.
-      </para>
-      <programlisting language="nix">
-# Enable nginx slice module distributed with Tengine
-services.nginx.package = pkgs.tengine;
-
-# Enable media proxy
-services.akkoma.config.&quot;:pleroma&quot;.&quot;:media_proxy&quot; = {
-  enabled = true;
-  proxy_opts.redirect_on_failure = true;
-};
-
-# Adjust the persistent cache size as needed:
-#  Assuming an average object size of 128 KiB, around 1 MiB
-#  of memory is required for the key zone per GiB of cache.
-# Ensure that the cache directory exists and is writable by nginx.
-services.nginx.commonHttpConfig = ''
-  proxy_cache_path /var/cache/nginx/cache/akkoma-media-cache
-    levels= keys_zone=akkoma_media_cache:16m max_size=16g
-    inactive=1y use_temp_path=off;
-'';
-
-services.akkoma.nginx = {
-  locations.&quot;/proxy&quot; = {
-    proxyPass = &quot;http://unix:/run/akkoma/socket&quot;;
-
-    extraConfig = ''
-      proxy_cache akkoma_media_cache;
-
-      # Cache objects in slices of 1 MiB
-      slice 1m;
-      proxy_cache_key $host$uri$is_args$args$slice_range;
-      proxy_set_header Range $slice_range;
-
-      # Decouple proxy and upstream responses
-      proxy_buffering on;
-      proxy_cache_lock on;
-      proxy_ignore_client_abort on;
-
-      # Default cache times for various responses
-      proxy_cache_valid 200 1y;
-      proxy_cache_valid 206 301 304 1h;
-
-      # Allow serving of stale items
-      proxy_cache_use_stale error timeout invalid_header updating;
-    '';
-  };
-};
-</programlisting>
-      <section xml:id="modules-services-akkoma-prefetch-remote-media">
-        <title>Prefetch remote media</title>
-        <para>
-          The following example enables the
-          <literal>MediaProxyWarmingPolicy</literal> MRF policy which
-          automatically fetches all media associated with a post through
-          the media proxy, as soon as the post is received by the
-          instance.
-        </para>
-        <programlisting language="nix">
-services.akkoma.config.&quot;:pleroma&quot;.&quot;:mrf&quot;.policies =
-  map (pkgs.formats.elixirConf { }).lib.mkRaw [
-    &quot;Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy&quot;
-];
-</programlisting>
-      </section>
-      <section xml:id="modules-services-akkoma-media-previews">
-        <title>Media previews</title>
-        <para>
-          Akkoma can generate previews for media.
-        </para>
-        <programlisting language="nix">
-services.akkoma.config.&quot;:pleroma&quot;.&quot;:media_preview_proxy&quot; = {
-  enabled = true;
-  thumbnail_max_width = 1920;
-  thumbnail_max_height = 1080;
-};
-</programlisting>
-      </section>
-    </section>
-  </section>
-  <section xml:id="modules-services-akkoma-frontend-management">
-    <title>Frontend management</title>
-    <para>
-      Akkoma will be deployed with the <literal>pleroma-fe</literal> and
-      <literal>admin-fe</literal> frontends by default. These can be
-      modified by setting
-      <link xlink:href="options.html#opt-services.akkoma.frontends"><option>services.akkoma.frontends</option></link>.
-    </para>
-    <para>
-      The following example overrides the primary frontend’s default
-      configuration using a custom derivation.
-    </para>
-    <programlisting language="nix">
-services.akkoma.frontends.primary.package = pkgs.runCommand &quot;pleroma-fe&quot; {
-  config = builtins.toJSON {
-    expertLevel = 1;
-    collapseMessageWithSubject = false;
-    stopGifs = false;
-    replyVisibility = &quot;following&quot;;
-    webPushHideIfCW = true;
-    hideScopeNotice = true;
-    renderMisskeyMarkdown = false;
-    hideSiteFavicon = true;
-    postContentType = &quot;text/markdown&quot;;
-    showNavShortcuts = false;
-  };
-  nativeBuildInputs = with pkgs; [ jq xorg.lndir ];
-  passAsFile = [ &quot;config&quot; ];
-} ''
-  mkdir $out
-  lndir ${pkgs.akkoma-frontends.pleroma-fe} $out
-
-  rm $out/static/config.json
-  jq -s add ${pkgs.akkoma-frontends.pleroma-fe}/static/config.json ${config} \
-    &gt;$out/static/config.json
-'';
-</programlisting>
-  </section>
-  <section xml:id="modules-services-akkoma-federation-policies">
-    <title>Federation policies</title>
-    <para>
-      Akkoma comes with a number of modules to police federation with
-      other ActivityPub instances. The most valuable for typical users
-      is the
-      <link xlink:href="https://docs.akkoma.dev/stable/configuration/cheatsheet/#mrf_simple"><literal>:mrf_simple</literal></link>
-      module which allows limiting federation based on instance
-      hostnames.
-    </para>
-    <para>
-      This configuration snippet provides an example on how these can be
-      used. Choosing an adequate federation policy is not trivial and
-      entails finding a balance between connectivity to the rest of the
-      fediverse and providing a pleasant experience to the users of an
-      instance.
-    </para>
-    <programlisting language="nix">
-services.akkoma.config.&quot;:pleroma&quot; = with (pkgs.formats.elixirConf { }).lib; {
-  &quot;:mrf&quot;.policies = map mkRaw [
-    &quot;Pleroma.Web.ActivityPub.MRF.SimplePolicy&quot;
-  ];
-
-  &quot;:mrf_simple&quot; = {
-    # Tag all media as sensitive
-    media_nsfw = mkMap {
-      &quot;nsfw.weird.kinky&quot; = &quot;Untagged NSFW content&quot;;
-    };
-
-    # Reject all activities except deletes
-    reject = mkMap {
-      &quot;kiwifarms.cc&quot; = &quot;Persistent harassment of users, no moderation&quot;;
-    };
-
-    # Force posts to be visible by followers only
-    followers_only = mkMap {
-      &quot;beta.birdsite.live&quot; = &quot;Avoid polluting timelines with Twitter posts&quot;;
-    };
-  };
-};
-</programlisting>
-  </section>
-  <section xml:id="modules-services-akkoma-upload-filters">
-    <title>Upload filters</title>
-    <para>
-      This example strips GPS and location metadata from uploads,
-      deduplicates them and anonymises the the file name.
-    </para>
-    <programlisting language="nix">
-services.akkoma.config.&quot;:pleroma&quot;.&quot;Pleroma.Upload&quot;.filters =
-  map (pkgs.formats.elixirConf { }).lib.mkRaw [
-    &quot;Pleroma.Upload.Filter.Exiftool&quot;
-    &quot;Pleroma.Upload.Filter.Dedupe&quot;
-    &quot;Pleroma.Upload.Filter.AnonymizeFilename&quot;
-  ];
-</programlisting>
-  </section>
-  <section xml:id="modules-services-akkoma-migration-pleroma">
-    <title>Migration from Pleroma</title>
-    <para>
-      Pleroma instances can be migrated to Akkoma either by copying the
-      database and upload data or by pointing Akkoma to the existing
-      data. The necessary database migrations are run automatically
-      during startup of the service.
-    </para>
-    <para>
-      The configuration has to be copy‐edited manually.
-    </para>
-    <para>
-      Depending on the size of the database, the initial migration may
-      take a long time and exceed the startup timeout of the system
-      manager. To work around this issue one may adjust the startup
-      timeout
-      <option>systemd.services.akkoma.serviceConfig.TimeoutStartSec</option>
-      or simply run the migrations manually:
-    </para>
-    <programlisting>
-pleroma_ctl migrate
-</programlisting>
-    <section xml:id="modules-services-akkoma-migration-pleroma-copy">
-      <title>Copying data</title>
-      <para>
-        Copying the Pleroma data instead of re‐using it in place may
-        permit easier reversion to Pleroma, but allows the two data sets
-        to diverge.
-      </para>
-      <para>
-        First disable Pleroma and then copy its database and upload
-        data:
-      </para>
-      <programlisting>
-# Create a copy of the database
-nix-shell -p postgresql --run 'createdb -T pleroma akkoma'
-
-# Copy upload data
-mkdir /var/lib/akkoma
-cp -R --reflink=auto /var/lib/pleroma/uploads /var/lib/akkoma/
-</programlisting>
-      <para>
-        After the data has been copied, enable the Akkoma service and
-        verify that the migration has been successful. If no longer
-        required, the original data may then be deleted:
-      </para>
-      <programlisting>
-# Delete original database
-nix-shell -p postgresql --run 'dropdb pleroma'
-
-# Delete original Pleroma state
-rm -r /var/lib/pleroma
-</programlisting>
-    </section>
-    <section xml:id="modules-services-akkoma-migration-pleroma-reuse">
-      <title>Re‐using data</title>
-      <para>
-        To re‐use the Pleroma data in place, disable Pleroma and enable
-        Akkoma, pointing it to the Pleroma database and upload
-        directory.
-      </para>
-      <programlisting language="nix">
-# Adjust these settings according to the database name and upload directory path used by Pleroma
-services.akkoma.config.&quot;:pleroma&quot;.&quot;Pleroma.Repo&quot;.database = &quot;pleroma&quot;;
-services.akkoma.config.&quot;:pleroma&quot;.&quot;:instance&quot;.upload_dir = &quot;/var/lib/pleroma/uploads&quot;;
-</programlisting>
-      <para>
-        Please keep in mind that after the Akkoma service has been
-        started, any migrations applied by Akkoma have to be rolled back
-        before the database can be used again with Pleroma. This can be
-        achieved through <literal>pleroma_ctl ecto.rollback</literal>.
-        Refer to the
-        <link xlink:href="https://hexdocs.pm/ecto_sql/Mix.Tasks.Ecto.Rollback.html">Ecto
-        SQL documentation</link> for details.
-      </para>
-    </section>
-  </section>
-  <section xml:id="modules-services-akkoma-advanced-deployment">
-    <title>Advanced deployment options</title>
-    <section xml:id="modules-services-akkoma-confinement">
-      <title>Confinement</title>
-      <para>
-        The Akkoma systemd service may be confined to a chroot with
-      </para>
-      <programlisting language="nix">
-services.systemd.akkoma.confinement.enable = true;
-</programlisting>
-      <para>
-        Confinement of services is not generally supported in NixOS and
-        therefore disabled by default. Depending on the Akkoma
-        configuration, the default confinement settings may be
-        insufficient and lead to subtle errors at run time, requiring
-        adjustment:
-      </para>
-      <para>
-        Use
-        <link xlink:href="options.html#opt-systemd.services._name_.confinement.packages"><option>services.systemd.akkoma.confinement.packages</option></link>
-        to make packages available in the chroot.
-      </para>
-      <para>
-        <option>services.systemd.akkoma.serviceConfig.BindPaths</option>
-        and
-        <option>services.systemd.akkoma.serviceConfig.BindReadOnlyPaths</option>
-        permit access to outside paths through bind mounts. Refer to
-        <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths="><link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html"><citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry></link></link>
-        for details.
-      </para>
-    </section>
-    <section xml:id="modules-services-akkoma-distributed-deployment">
-      <title>Distributed deployment</title>
-      <para>
-        Being an Elixir application, Akkoma can be deployed in a
-        distributed fashion.
-      </para>
-      <para>
-        This requires setting
-        <link xlink:href="options.html#opt-services.akkoma.dist.address"><option>services.akkoma.dist.address</option></link>
-        and
-        <link xlink:href="options.html#opt-services.akkoma.dist.cookie"><option>services.akkoma.dist.cookie</option></link>.
-        The specifics depend strongly on the deployment environment. For
-        more information please check the relevant
-        <link xlink:href="https://www.erlang.org/doc/reference_manual/distributed.html">Erlang
-        documentation</link>.
-      </para>
-    </section>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/alps.nix b/nixos/modules/services/web-apps/alps.nix
index 1a58df2da1d..05fb676102d 100644
--- a/nixos/modules/services/web-apps/alps.nix
+++ b/nixos/modules/services/web-apps/alps.nix
@@ -84,7 +84,7 @@ in {
         "-addr" "${cfg.bindIP}:${toString cfg.port}"
         "-theme" "${cfg.theme}"
         "imaps://${cfg.imaps.host}:${toString cfg.imaps.port}"
-        "smpts://${cfg.smtps.host}:${toString cfg.smtps.port}"
+        "smtps://${cfg.smtps.host}:${toString cfg.smtps.port}"
       ];
     };
   };
diff --git a/nixos/modules/services/web-apps/anuko-time-tracker.nix b/nixos/modules/services/web-apps/anuko-time-tracker.nix
new file mode 100644
index 00000000000..f43cbc40ec7
--- /dev/null
+++ b/nixos/modules/services/web-apps/anuko-time-tracker.nix
@@ -0,0 +1,388 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.services.anuko-time-tracker;
+  configFile = let
+    smtpPassword = if cfg.settings.email.smtpPasswordFile == null
+                   then "''"
+                   else "trim(file_get_contents('${cfg.settings.email.smtpPasswordFile}'))";
+
+  in pkgs.writeText "config.php" ''
+    <?php
+    // Set include path for PEAR and its modules, which we include in the distribution.
+    // Updated for the correct location in the nix store.
+    set_include_path('${cfg.package}/WEB-INF/lib/pear' . PATH_SEPARATOR . get_include_path());
+    define('DSN', 'mysqli://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}?charset=utf8mb4');
+    define('MULTIORG_MODE', ${lib.boolToString cfg.settings.multiorgMode});
+    define('EMAIL_REQUIRED', ${lib.boolToString cfg.settings.emailRequired});
+    define('WEEKEND_START_DAY', ${toString cfg.settings.weekendStartDay});
+    define('FORUM_LINK', '${cfg.settings.forumLink}');
+    define('HELP_LINK', '${cfg.settings.helpLink}');
+    define('SENDER', '${cfg.settings.email.sender}');
+    define('MAIL_MODE', '${cfg.settings.email.mode}');
+    define('MAIL_SMTP_HOST', '${toString cfg.settings.email.smtpHost}');
+    define('MAIL_SMTP_PORT', '${toString cfg.settings.email.smtpPort}');
+    define('MAIL_SMTP_USER', '${cfg.settings.email.smtpUser}');
+    define('MAIL_SMTP_PASSWORD', ${smtpPassword});
+    define('MAIL_SMTP_AUTH', ${lib.boolToString cfg.settings.email.smtpAuth});
+    define('MAIL_SMTP_DEBUG', ${lib.boolToString cfg.settings.email.smtpDebug});
+    define('DEFAULT_CSS', 'default.css');
+    define('RTL_CSS', 'rtl.css'); // For right to left languages.
+    define('LANG_DEFAULT', '${cfg.settings.defaultLanguage}');
+    define('CURRENCY_DEFAULT', '${cfg.settings.defaultCurrency}');
+    define('EXPORT_DECIMAL_DURATION', ${lib.boolToString cfg.settings.exportDecimalDuration});
+    define('REPORT_FOOTER', ${lib.boolToString cfg.settings.reportFooter});
+    define('AUTH_MODULE', 'db');
+  '';
+  package = pkgs.stdenv.mkDerivation rec {
+    pname = "anuko-time-tracker";
+    inherit (src) version;
+    src = cfg.package;
+    installPhase = ''
+      mkdir -p $out
+      cp -r * $out/
+
+      # Link config file
+      ln -s ${configFile} $out/WEB-INF/config.php
+
+      # Link writable templates_c directory
+      rm -rf $out/WEB-INF/templates_c
+      ln -s ${cfg.dataDir}/templates_c $out/WEB-INF/templates_c
+
+      # Remove unsafe dbinstall.php
+      rm -f $out/dbinstall.php
+    '';
+  };
+in
+{
+  options.services.anuko-time-tracker = {
+    enable = lib.mkEnableOption (lib.mdDoc "Anuko Time Tracker");
+
+    package = lib.mkPackageOptionMD pkgs "anuko-time-tracker" {};
+
+    database = {
+      createLocally = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = lib.mdDoc "Create the database and database user locally.";
+      };
+
+      host = lib.mkOption {
+        type = lib.types.str;
+        description = lib.mdDoc "Database host.";
+        default = "localhost";
+      };
+
+      name = lib.mkOption {
+        type = lib.types.str;
+        description = lib.mdDoc "Database name.";
+        default = "anuko_time_tracker";
+      };
+
+      user = lib.mkOption {
+        type = lib.types.str;
+        description = lib.mdDoc "Database username.";
+        default = "anuko_time_tracker";
+      };
+
+      passwordFile = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        description = lib.mdDoc "Database user password file.";
+        default = null;
+      };
+    };
+
+    poolConfig = lib.mkOption {
+      type = lib.types.attrsOf (lib.types.oneOf [ lib.types.str lib.types.int lib.types.bool ]);
+      default = {
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 4;
+        "pm.max_requests" = 500;
+      };
+      description = lib.mdDoc ''
+        Options for Anuko Time Tracker's PHP-FPM pool.
+      '';
+    };
+
+    hostname = lib.mkOption {
+      type = lib.types.str;
+      default =
+        if config.networking.domain != null
+        then config.networking.fqdn
+        else config.networking.hostName;
+      defaultText = lib.literalExpression "config.networking.fqdn";
+      example = "anuko.example.com";
+      description = lib.mdDoc ''
+        The hostname to serve Anuko Time Tracker on.
+      '';
+    };
+
+    nginx = lib.mkOption {
+      type = lib.types.submodule (
+        lib.recursiveUpdate
+          (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {}
+      );
+      default = {};
+      example = lib.literalExpression ''
+        {
+          serverAliases = [
+            "anuko.''${config.networking.domain}"
+          ];
+
+          # To enable encryption and let let's encrypt take care of certificate
+          forceSSL = true;
+          enableACME = true;
+        }
+      '';
+      description = lib.mdDoc ''
+        With this option, you can customize the Nginx virtualHost settings.
+      '';
+    };
+
+    dataDir = lib.mkOption {
+      type = lib.types.str;
+      default = "/var/lib/anuko-time-tracker";
+      description = lib.mdDoc "Default data folder for Anuko Time Tracker.";
+      example = "/mnt/anuko-time-tracker";
+    };
+
+    user = lib.mkOption {
+      type = lib.types.str;
+      default = "anuko_time_tracker";
+      description = lib.mdDoc "User under which Anuko Time Tracker runs.";
+    };
+
+    settings = {
+      multiorgMode = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = lib.mdDoc ''
+          Defines whether users see the Register option in the menu of Time Tracker that allows them
+          to self-register and create new organizations (top groups).
+        '';
+      };
+
+      emailRequired = lib.mkOption {
+        type = lib.types.bool;
+        default = false;
+        description = lib.mdDoc "Defines whether an email is required for new registrations.";
+      };
+
+      weekendStartDay = lib.mkOption {
+        type = lib.types.int;
+        default = 6;
+        description = lib.mdDoc ''
+          This option defines which days are highlighted with weekend color.
+          6 means Saturday. For Saudi Arabia, etc. set it to 4 for Thursday and Friday to be
+          weekend days.
+        '';
+      };
+
+      forumLink = lib.mkOption {
+        type = lib.types.str;
+        description = lib.mdDoc "Forum link from the main menu.";
+        default = "https://www.anuko.com/forum/viewforum.php?f=4";
+      };
+
+      helpLink = lib.mkOption {
+        type = lib.types.str;
+        description = lib.mdDoc "Help link from the main menu.";
+        default = "https://www.anuko.com/time-tracker/user-guide/index.htm";
+      };
+
+      email = {
+        sender = lib.mkOption {
+          type = lib.types.str;
+          description = lib.mdDoc "Default sender for mail.";
+          default = "Anuko Time Tracker <bounces@example.com>";
+        };
+
+        mode = lib.mkOption {
+          type = lib.types.str;
+          description = lib.mdDoc "Mail sending mode. Can be 'mail' or 'smtp'.";
+          default = "smtp";
+        };
+
+        smtpHost = lib.mkOption {
+          type = lib.types.str;
+          description = lib.mdDoc "MTA hostname.";
+          default = "localhost";
+        };
+
+        smtpPort = lib.mkOption {
+          type = lib.types.int;
+          description = lib.mdDoc "MTA port.";
+          default = 25;
+        };
+
+        smtpUser = lib.mkOption {
+          type = lib.types.str;
+          description = lib.mdDoc "MTA authentication username.";
+          default = "";
+        };
+
+        smtpAuth = lib.mkOption {
+          type = lib.types.bool;
+          default = false;
+          description = lib.mdDoc "MTA requires authentication.";
+        };
+
+        smtpPasswordFile = lib.mkOption {
+          type = lib.types.nullOr lib.types.path;
+          default = null;
+          example = "/var/lib/anuko-time-tracker/secrets/smtp-password";
+          description = lib.mdDoc ''
+            Path to file containing the MTA authentication password.
+          '';
+        };
+
+        smtpDebug = lib.mkOption {
+          type = lib.types.bool;
+          default = false;
+          description = lib.mdDoc "Debug mail sending.";
+        };
+      };
+
+      defaultLanguage = lib.mkOption {
+        type = lib.types.str;
+        description = lib.mdDoc ''
+          Defines Anuko Time Tracker default language. It is used on Time Tracker login page.
+          After login, a language set for user group is used.
+          Empty string means the language is defined by user browser.
+        '';
+        default = "";
+        example = "nl";
+      };
+
+      defaultCurrency = lib.mkOption {
+        type = lib.types.str;
+        description = lib.mdDoc ''
+          Defines a default currency symbol for new groups.
+          Use €, £, a more specific dollar like US$, CAD, etc.
+        '';
+        default = "$";
+        example = "€";
+      };
+
+      exportDecimalDuration = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = lib.mdDoc ''
+          Defines whether time duration values are decimal in CSV and XML data
+          exports (1.25 vs 1:15).
+        '';
+      };
+
+      reportFooter = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = lib.mdDoc "Defines whether to use a footer on reports.";
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = ''
+          <option>services.anuko-time-tracker.database.passwordFile</option> cannot be specified if
+          <option>services.anuko-time-tracker.database.createLocally</option> is set to true.
+        '';
+      }
+      {
+        assertion = cfg.settings.email.smtpAuth -> (cfg.settings.email.smtpPasswordFile != null);
+        message = ''
+          <option>services.anuko-time-tracker.settings.email.smtpPasswordFile</option> needs to be set if
+          <option>services.anuko-time-tracker.settings.email.smtpAuth</option> is enabled.
+        '';
+      }
+    ];
+
+    services.phpfpm = {
+      pools.anuko-time-tracker = {
+        inherit (cfg) user;
+        group = config.services.nginx.group;
+        settings = {
+          "listen.owner" = config.services.nginx.user;
+          "listen.group" = config.services.nginx.group;
+        } // cfg.poolConfig;
+      };
+    };
+
+    services.nginx = {
+      enable = lib.mkDefault true;
+      recommendedTlsSettings = true;
+      recommendedOptimisation = true;
+      recommendedGzipSettings = true;
+      virtualHosts."${cfg.hostname}" = lib.mkMerge [
+        cfg.nginx
+        {
+          root = lib.mkForce "${package}";
+          locations = {
+            "/".index = "index.php";
+            "~ [^/]\\.php(/|$)" = {
+              extraConfig = ''
+                fastcgi_split_path_info ^(.+?\.php)(/.*)$;
+                fastcgi_pass unix:${config.services.phpfpm.pools.anuko-time-tracker.socket};
+              '';
+            };
+          };
+        }
+      ];
+    };
+
+    services.mysql = lib.mkIf cfg.database.createLocally {
+      enable = lib.mkDefault true;
+      package = lib.mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [{
+        name = cfg.database.user;
+        ensurePermissions = {
+          "${cfg.database.name}.*" = "ALL PRIVILEGES";
+        };
+      }];
+    };
+
+    systemd = {
+      services = {
+        anuko-time-tracker-setup-database = lib.mkIf cfg.database.createLocally {
+          description = "Set up Anuko Time Tracker database";
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+          };
+          wantedBy = [ "phpfpm-anuko-time-tracker.service" ];
+          after = [ "mysql.service" ];
+          script =
+            let
+              mysql = "${config.services.mysql.package}/bin/mysql";
+            in
+            ''
+              if [ ! -f ${cfg.dataDir}/.dbexists ]; then
+                # Load database schema provided with package
+                ${mysql} ${cfg.database.name} < ${cfg.package}/mysql.sql
+
+                touch ${cfg.dataDir}/.dbexists
+              fi
+            '';
+        };
+      };
+      tmpfiles.rules = [
+        "d ${cfg.dataDir} 0750 ${cfg.user} ${config.services.nginx.group} -"
+        "d ${cfg.dataDir}/templates_c 0750 ${cfg.user} ${config.services.nginx.group} -"
+      ];
+    };
+
+    users.users."${cfg.user}" = {
+      isSystemUser = true;
+      group = config.services.nginx.group;
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ michaelshmitty ];
+}
diff --git a/nixos/modules/services/web-apps/baget.nix b/nixos/modules/services/web-apps/baget.nix
deleted file mode 100644
index e4d5a1faddb..00000000000
--- a/nixos/modules/services/web-apps/baget.nix
+++ /dev/null
@@ -1,170 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.baget;
-
-  defaultConfig = {
-    "PackageDeletionBehavior" = "Unlist";
-    "AllowPackageOverwrites" = false;
-
-    "Database" = {
-      "Type" = "Sqlite";
-      "ConnectionString" = "Data Source=baget.db";
-    };
-
-    "Storage" = {
-      "Type" = "FileSystem";
-      "Path" = "";
-    };
-
-    "Search" = {
-      "Type" = "Database";
-    };
-
-    "Mirror" = {
-      "Enabled" = false;
-      "PackageSource" = "https://api.nuget.org/v3/index.json";
-    };
-
-    "Logging" = {
-      "IncludeScopes" = false;
-      "Debug" = {
-        "LogLevel" = {
-          "Default" = "Warning";
-        };
-      };
-      "Console" = {
-        "LogLevel" = {
-          "Microsoft.Hosting.Lifetime" = "Information";
-          "Default" = "Warning";
-        };
-      };
-    };
-  };
-
-  configAttrs = recursiveUpdate defaultConfig cfg.extraConfig;
-
-  configFormat = pkgs.formats.json {};
-  configFile = configFormat.generate "appsettings.json" configAttrs;
-
-in
-{
-  options.services.baget = {
-    enable = mkEnableOption (lib.mdDoc "BaGet NuGet-compatible server");
-
-    apiKeyFile = mkOption {
-      type = types.path;
-      example = "/root/baget.key";
-      description = lib.mdDoc ''
-        Private API key for BaGet.
-      '';
-    };
-
-    extraConfig = mkOption {
-      type = configFormat.type;
-      default = {};
-      example = {
-        "Database" = {
-          "Type" = "PostgreSql";
-          "ConnectionString" = "Server=/run/postgresql;Port=5432;";
-        };
-      };
-      defaultText = literalExpression ''
-        {
-          "PackageDeletionBehavior" = "Unlist";
-          "AllowPackageOverwrites" = false;
-
-          "Database" = {
-            "Type" = "Sqlite";
-            "ConnectionString" = "Data Source=baget.db";
-          };
-
-          "Storage" = {
-            "Type" = "FileSystem";
-            "Path" = "";
-          };
-
-          "Search" = {
-            "Type" = "Database";
-          };
-
-          "Mirror" = {
-            "Enabled" = false;
-            "PackageSource" = "https://api.nuget.org/v3/index.json";
-          };
-
-          "Logging" = {
-            "IncludeScopes" = false;
-            "Debug" = {
-              "LogLevel" = {
-                "Default" = "Warning";
-              };
-            };
-            "Console" = {
-              "LogLevel" = {
-                "Microsoft.Hosting.Lifetime" = "Information";
-                "Default" = "Warning";
-              };
-            };
-          };
-        }
-      '';
-      description = lib.mdDoc ''
-        Extra configuration options for BaGet. Refer to <https://loic-sharma.github.io/BaGet/configuration/> for details.
-        Default value is merged with values from here.
-      '';
-    };
-  };
-
-  # implementation
-
-  config = mkIf cfg.enable {
-
-    systemd.services.baget = {
-      description = "BaGet server";
-      wantedBy = [ "multi-user.target" ];
-      wants = [ "network-online.target" ];
-      after = [ "network.target" "network-online.target" ];
-      path = [ pkgs.jq ];
-      serviceConfig = {
-        WorkingDirectory = "/var/lib/baget";
-        DynamicUser = true;
-        StateDirectory = "baget";
-        StateDirectoryMode = "0700";
-        LoadCredential = "api_key:${cfg.apiKeyFile}";
-
-        CapabilityBoundingSet = "";
-        NoNewPrivileges = true;
-        PrivateDevices = true;
-        PrivateTmp = true;
-        PrivateUsers = true;
-        PrivateMounts = true;
-        ProtectHome = true;
-        ProtectClock = true;
-        ProtectProc = "noaccess";
-        ProcSubset = "pid";
-        ProtectKernelLogs = true;
-        ProtectKernelModules = true;
-        ProtectKernelTunables = true;
-        ProtectControlGroups = true;
-        ProtectHostname = true;
-        RestrictSUIDSGID = true;
-        RestrictRealtime = true;
-        RestrictNamespaces = true;
-        LockPersonality = true;
-        RemoveIPC = true;
-        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
-        SystemCallFilter = [ "@system-service" "~@privileged" ];
-      };
-      script = ''
-        jq --slurpfile apiKeys <(jq -R . "$CREDENTIALS_DIRECTORY/api_key") '.ApiKey = $apiKeys[0]' ${configFile} > appsettings.json
-        ln -snf ${pkgs.baget}/lib/BaGet/wwwroot wwwroot
-        exec ${pkgs.baget}/bin/BaGet
-      '';
-    };
-
-  };
-}
diff --git a/nixos/modules/services/web-apps/changedetection-io.nix b/nixos/modules/services/web-apps/changedetection-io.nix
index fc00aee4351..bbf4c2aed18 100644
--- a/nixos/modules/services/web-apps/changedetection-io.nix
+++ b/nixos/modules/services/web-apps/changedetection-io.nix
@@ -214,7 +214,7 @@ in
           };
         })
       ];
-      podman.defaultNetwork.dnsname.enable = true;
+      podman.defaultNetwork.settings.dns_enabled = true;
     };
   };
 }
diff --git a/nixos/modules/services/web-apps/chatgpt-retrieval-plugin.nix b/nixos/modules/services/web-apps/chatgpt-retrieval-plugin.nix
new file mode 100644
index 00000000000..f29d095bc10
--- /dev/null
+++ b/nixos/modules/services/web-apps/chatgpt-retrieval-plugin.nix
@@ -0,0 +1,106 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.chatgpt-retrieval-plugin;
+in
+{
+  options.services.chatgpt-retrieval-plugin = {
+    enable = mkEnableOption (lib.mdDoc "chatgpt-retrieval-plugin service");
+
+    port = mkOption {
+      type = types.port;
+      default = 8080;
+      description = lib.mdDoc "Port the chatgpt-retrieval-plugin service listens on.";
+    };
+
+    host = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      example = "0.0.0.0";
+      description = lib.mdDoc "The hostname or IP address for chatgpt-retrieval-plugin to bind to.";
+    };
+
+    bearerTokenPath = mkOption {
+      type = types.path;
+      description = lib.mdDoc ''
+        Path to the secret bearer token used for the http api authentication.
+      '';
+      default = "";
+      example = "config.age.secrets.CHATGPT_RETRIEVAL_PLUGIN_BEARER_TOKEN.path";
+    };
+
+    openaiApiKeyPath = mkOption {
+      type = types.path;
+      description = lib.mdDoc ''
+        Path to the secret openai api key used for embeddings.
+      '';
+      default = "";
+      example = "config.age.secrets.CHATGPT_RETRIEVAL_PLUGIN_OPENAI_API_KEY.path";
+    };
+
+    datastore = mkOption {
+      type = types.enum [ "pinecone" "weaviate" "zilliz" "milvus" "qdrant" "redis" ];
+      default = "qdrant";
+      description = lib.mdDoc "This specifies the vector database provider you want to use to store and query embeddings.";
+    };
+
+    qdrantCollection = mkOption {
+      type = types.str;
+      description = lib.mdDoc ''
+        name of the qdrant collection used to store documents.
+      '';
+      default = "document_chunks";
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = cfg.bearerTokenPath != "";
+        message = "services.chatgpt-retrieval-plugin.bearerTokenPath should not be an empty string.";
+      }
+      {
+        assertion = cfg.openaiApiKeyPath != "";
+        message = "services.chatgpt-retrieval-plugin.openaiApiKeyPath should not be an empty string.";
+      }
+    ];
+
+    systemd.services.chatgpt-retrieval-plugin = {
+      description = "ChatGPT Retrieval Plugin";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        LoadCredential = [
+          "BEARER_TOKEN:${cfg.bearerTokenPath}"
+          "OPENAI_API_KEY:${cfg.openaiApiKeyPath}"
+        ];
+        StateDirectory = "chatgpt-retrieval-plugin";
+        StateDirectoryMode = "0755";
+      };
+
+      # it doesn't make sense to pass secrets as env vars, this is a hack until
+      # upstream has proper secret management.
+      script = ''
+        export BEARER_TOKEN=$(${pkgs.systemd}/bin/systemd-creds cat BEARER_TOKEN)
+        export OPENAI_API_KEY=$(${pkgs.systemd}/bin/systemd-creds cat OPENAI_API_KEY)
+        exec ${pkgs.chatgpt-retrieval-plugin}/bin/start --host ${cfg.host} --port ${toString cfg.port}
+      '';
+
+      environment = {
+        DATASTORE = cfg.datastore;
+        QDRANT_COLLECTION = mkIf (cfg.datastore == "qdrant") cfg.qdrantCollection;
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      # create the directory for static files for fastapi
+      "C /var/lib/chatgpt-retrieval-plugin/.well-known - - - - ${pkgs.chatgpt-retrieval-plugin}/${pkgs.python3Packages.python.sitePackages}/.well-known"
+    ];
+  };
+}
diff --git a/nixos/modules/services/web-apps/cloudlog.nix b/nixos/modules/services/web-apps/cloudlog.nix
new file mode 100644
index 00000000000..da2cf93d7f1
--- /dev/null
+++ b/nixos/modules/services/web-apps/cloudlog.nix
@@ -0,0 +1,503 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cloudlog;
+  dbFile = let
+    password = if cfg.database.createLocally
+               then "''"
+               else "trim(file_get_contents('${cfg.database.passwordFile}'))";
+  in pkgs.writeText "database.php" ''
+    <?php
+    defined('BASEPATH') OR exit('No direct script access allowed');
+    $active_group = 'default';
+    $query_builder = TRUE;
+    $db['default'] = array(
+      'dsn' => "",
+      'hostname' => '${cfg.database.host}',
+      'username' => '${cfg.database.user}',
+      'password' => ${password},
+      'database' => '${cfg.database.name}',
+      'dbdriver' => 'mysqli',
+      'dbprefix' => "",
+      'pconnect' => TRUE,
+      'db_debug' => (ENVIRONMENT !== 'production'),
+      'cache_on' => FALSE,
+      'cachedir' => "",
+      'char_set' => 'utf8mb4',
+      'dbcollat' => 'utf8mb4_general_ci',
+      'swap_pre' => "",
+      'encrypt' => FALSE,
+      'compress' => FALSE,
+      'stricton' => FALSE,
+      'failover' => array(),
+      'save_queries' => TRUE
+    );
+  '';
+  configFile = pkgs.writeText "config.php" ''
+    <?php
+    include('${pkgs.cloudlog}/install/config/config.php');
+    $config['datadir'] = "${cfg.dataDir}/";
+    $config['base_url'] = "${cfg.baseUrl}";
+    ${cfg.extraConfig}
+  '';
+  package = pkgs.stdenv.mkDerivation rec {
+    pname = "cloudlog";
+    version = src.version;
+    src = pkgs.cloudlog;
+    installPhase = ''
+      mkdir -p $out
+      cp -r * $out/
+
+      ln -s ${configFile} $out/application/config/config.php
+      ln -s ${dbFile} $out/application/config/database.php
+
+      # link writable directories
+      for directory in updates uploads backup logbook; do
+        rm -rf $out/$directory
+        ln -s ${cfg.dataDir}/$directory $out/$directory
+      done
+
+      # link writable asset files
+      for asset in dok sota wwff; do
+        rm -rf $out/assets/json/$asset.txt
+        ln -s ${cfg.dataDir}/assets/json/$asset.txt $out/assets/json/$asset.txt
+      done
+    '';
+  };
+in
+{
+  options.services.cloudlog = with types; {
+    enable = mkEnableOption (mdDoc "Whether to enable Cloudlog");
+    dataDir = mkOption {
+      type = str;
+      default = "/var/lib/cloudlog";
+      description = mdDoc "Cloudlog data directory.";
+    };
+    baseUrl = mkOption {
+      type = str;
+      default = "http://localhost";
+      description = mdDoc "Cloudlog base URL";
+    };
+    user = mkOption {
+      type = str;
+      default = "cloudlog";
+      description = mdDoc "User account under which Cloudlog runs.";
+    };
+    database = {
+      createLocally = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Create the database and database user locally.";
+      };
+      host = mkOption {
+        type = str;
+        description = mdDoc "MySQL database host";
+        default = "localhost";
+      };
+      name = mkOption {
+        type = str;
+        description = mdDoc "MySQL database name.";
+        default = "cloudlog";
+      };
+      user = mkOption {
+        type = str;
+        description = mdDoc "MySQL user name.";
+        default = "cloudlog";
+      };
+      passwordFile = mkOption {
+        type = nullOr str;
+        description = mdDoc "MySQL user password file.";
+        default = null;
+      };
+    };
+    poolConfig = mkOption {
+      type = attrsOf (oneOf [ str int bool ]);
+      default = {
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 4;
+        "pm.max_requests" = 500;
+      };
+      description = mdDoc ''
+        Options for Cloudlog's PHP-FPM pool.
+      '';
+    };
+    virtualHost = mkOption {
+      type = nullOr str;
+      default = "localhost";
+      description = mdDoc ''
+        Name of the nginx virtualhost to use and setup. If null, do not setup
+         any virtualhost.
+      '';
+    };
+    extraConfig = mkOption {
+      description = mdDoc ''
+       Any additional text to be appended to the config.php
+       configuration file. This is a PHP script. For configuration
+       settings, see <https://github.com/magicbug/Cloudlog/wiki/Cloudlog.php-Configuration-File>.
+      '';
+      default = "";
+      type = str;
+      example = ''
+        $config['show_time'] = TRUE;
+      '';
+    };
+    upload-lotw = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically upload logs to LoTW. If enabled, a systemd
+          timer will run the log upload task as specified by the interval
+           option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "daily";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the
+          time at which the LoTW upload will occur.
+        '';
+      };
+    };
+    upload-clublog = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically upload logs to Clublog. If enabled, a systemd
+          timer will run the log upload task as specified by the interval option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "daily";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the time
+          at which the Clublog upload will occur.
+        '';
+      };
+    };
+    update-lotw-users = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically update the list of LoTW users. If enabled, a
+          systemd timer will run the update task as specified by the interval
+          option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "weekly";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the
+          time at which the LoTW user update will occur.
+        '';
+      };
+    };
+    update-dok = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically update the DOK resource file. If enabled, a
+          systemd timer will run the update task as specified by the interval option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "monthly";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the
+          time at which the DOK update will occur.
+        '';
+      };
+    };
+    update-clublog-scp = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically update the Clublog SCP database. If enabled,
+          a systemd timer will run the update task as specified by the interval
+          option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "monthly";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the time
+          at which the Clublog SCP update will occur.
+        '';
+      };
+    };
+    update-wwff = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically update the WWFF database. If enabled, a
+          systemd timer will run the update task as specified by the interval
+          option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "monthly";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the time
+          at which the WWFF update will occur.
+        '';
+      };
+    };
+    upload-qrz = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically upload logs to QRZ. If enabled, a systemd
+          timer will run the update task as specified by the interval option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "daily";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the
+          time at which the QRZ upload will occur.
+        '';
+      };
+    };
+    update-sota = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically update the SOTA database. If enabled, a
+          systemd timer will run the update task as specified by the interval option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "monthly";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the time
+          at which the SOTA update will occur.
+        '';
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = "services.cloudlog.database.passwordFile cannot be specified if services.cloudlog.database.createLocally is set to true.";
+      }
+    ];
+
+    services.phpfpm = {
+      pools.cloudlog = {
+        inherit (cfg) user;
+        group = config.services.nginx.group;
+        settings =  {
+          "listen.owner" = config.services.nginx.user;
+          "listen.group" = config.services.nginx.group;
+        } // cfg.poolConfig;
+      };
+    };
+
+    services.nginx = mkIf (cfg.virtualHost != null) {
+      enable = true;
+      virtualHosts = {
+        "${cfg.virtualHost}" = {
+          root = "${package}";
+          locations."/".tryFiles = "$uri /index.php$is_args$args";
+          locations."~ ^/index.php(/|$)".extraConfig = ''
+              include ${config.services.nginx.package}/conf/fastcgi_params;
+              include ${pkgs.nginx}/conf/fastcgi.conf;
+              fastcgi_split_path_info ^(.+\.php)(.+)$;
+              fastcgi_pass unix:${config.services.phpfpm.pools.cloudlog.socket};
+              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+            '';
+        };
+      };
+    };
+
+    services.mysql = mkIf cfg.database.createLocally {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [{
+        name = cfg.database.user;
+        ensurePermissions = {
+          "${cfg.database.name}.*" = "ALL PRIVILEGES";
+        };
+      }];
+    };
+
+    systemd = {
+      services = {
+        cloudlog-setup-database = mkIf cfg.database.createLocally {
+          description = "Set up cloudlog database";
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+          };
+          wantedBy = [ "phpfpm-cloudlog.service" ];
+          after = [ "mysql.service" ];
+          script = let
+            mysql = "${config.services.mysql.package}/bin/mysql";
+          in ''
+            if [ ! -f ${cfg.dataDir}/.dbexists ]; then
+              ${mysql} ${cfg.database.name} < ${pkgs.cloudlog}/install/assets/install.sql
+              touch ${cfg.dataDir}/.dbexists
+            fi
+        '';
+        };
+        cloudlog-upload-lotw = {
+          description = "Upload QSOs to LoTW if certs have been provided";
+          enable = cfg.upload-lotw.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/lotw/lotw_upload";
+        };
+        cloudlog-update-lotw-users = {
+          description = "Update LOTW Users Database";
+          enable = cfg.update-lotw-users.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/lotw/load_users";
+        };
+        cloudlog-update-dok = {
+          description = "Update DOK File for autocomplete";
+          enable = cfg.update-dok.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/update/update_dok";
+        };
+        cloudlog-update-clublog-scp = {
+          description = "Update Clublog SCP Database File";
+          enable = cfg.update-clublog-scp.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/update/update_clublog_scp";
+        };
+        cloudlog-update-wwff = {
+          description = "Update WWFF File for autocomplete";
+          enable = cfg.update-wwff.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/update/update_wwff";
+        };
+        cloudlog-upload-qrz = {
+          description = "Upload QSOs to QRZ Logbook";
+          enable = cfg.upload-qrz.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/qrz/upload";
+        };
+        cloudlog-update-sota = {
+          description = "Update SOTA File for autocomplete";
+          enable = cfg.update-sota.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/update/update_sota";
+        };
+      };
+      timers = {
+        cloudlog-upload-lotw = {
+          enable = cfg.upload-lotw.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-upload-lotw.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.upload-lotw.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-upload-clublog = {
+          enable = cfg.upload-clublog.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-upload-clublog.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.upload-clublog.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-update-lotw-users = {
+          enable = cfg.update-lotw-users.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-update-lotw-users.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.update-lotw-users.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-update-dok = {
+          enable = cfg.update-dok.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-update-dok.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.update-dok.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-update-clublog-scp = {
+          enable = cfg.update-clublog-scp.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-update-clublog-scp.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.update-clublog-scp.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-update-wwff =  {
+          enable = cfg.update-wwff.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-update-wwff.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.update-wwff.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-upload-qrz = {
+          enable = cfg.upload-qrz.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-upload-qrz.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.upload-qrz.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-update-sota = {
+          enable = cfg.update-sota.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-update-sota.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.update-sota.interval;
+            Persistent = true;
+          };
+        };
+      };
+      tmpfiles.rules = let
+        group = config.services.nginx.group;
+      in [
+        "d ${cfg.dataDir}                0750 ${cfg.user} ${group} - -"
+        "d ${cfg.dataDir}/updates        0750 ${cfg.user} ${group} - -"
+        "d ${cfg.dataDir}/uploads        0750 ${cfg.user} ${group} - -"
+        "d ${cfg.dataDir}/backup         0750 ${cfg.user} ${group} - -"
+        "d ${cfg.dataDir}/logbook        0750 ${cfg.user} ${group} - -"
+        "d ${cfg.dataDir}/assets/json    0750 ${cfg.user} ${group} - -"
+        "d ${cfg.dataDir}/assets/qslcard 0750 ${cfg.user} ${group} - -"
+      ];
+    };
+
+    users.users."${cfg.user}" = {
+      isSystemUser = true;
+      group = config.services.nginx.group;
+    };
+  };
+
+  meta.maintainers = with maintainers; [ melling ];
+}
diff --git a/nixos/modules/services/web-apps/code-server.nix b/nixos/modules/services/web-apps/code-server.nix
index 24e34e0c583..11601f6c304 100644
--- a/nixos/modules/services/web-apps/code-server.nix
+++ b/nixos/modules/services/web-apps/code-server.nix
@@ -1,107 +1,209 @@
 { config, lib, pkgs, ... }:
 
-with lib;
 let
-
   cfg = config.services.code-server;
   defaultUser = "code-server";
   defaultGroup = defaultUser;
-
 in {
-  ###### interface
   options = {
     services.code-server = {
-      enable = mkEnableOption (lib.mdDoc "code-server");
+      enable = lib.mkEnableOption (lib.mdDoc "code-server");
 
-      package = mkOption {
-        default = pkgs.code-server;
-        defaultText = lib.literalExpression "pkgs.code-server";
-        description = lib.mdDoc "Which code-server derivation to use.";
-        type = types.package;
+      package = lib.mkPackageOptionMD pkgs "code-server" {
+        example = ''
+          pkgs.vscode-with-extensions.override {
+            vscode = pkgs.code-server;
+            vscodeExtensions = with pkgs.vscode-extensions; [
+              bbenoist.nix
+              dracula-theme.theme-dracula
+            ];
+          }
+        '';
       };
 
-      extraPackages = mkOption {
+      extraPackages = lib.mkOption {
         default = [ ];
-        description = lib.mdDoc "Packages that are available in the PATH of code-server.";
-        example = "[ pkgs.go ]";
-        type = types.listOf types.package;
+        description = lib.mdDoc ''
+          Additional packages to add to the code-server {env}`PATH`.
+        '';
+        example = lib.literalExpression "[ pkgs.go ]";
+        type = lib.types.listOf lib.types.package;
       };
 
-      extraEnvironment = mkOption {
-        type = types.attrsOf types.str;
-        description =
-          lib.mdDoc "Additional environment variables to passed to code-server.";
+      extraEnvironment = lib.mkOption {
+        type = lib.types.attrsOf lib.types.str;
+        description = lib.mdDoc ''
+          Additional environment variables to pass to code-server.
+        '';
         default = { };
         example = { PKG_CONFIG_PATH = "/run/current-system/sw/lib/pkgconfig"; };
       };
 
-      extraArguments = mkOption {
-        default = [ "--disable-telemetry" ];
-        description = lib.mdDoc "Additional arguments that passed to code-server";
-        example = ''[ "--verbose" ]'';
-        type = types.listOf types.str;
+      extraArguments = lib.mkOption {
+        default = [ ];
+        description = lib.mdDoc ''
+          Additional arguments to pass to code-server.
+        '';
+        example = lib.literalExpression ''[ "--log=info" ]'';
+        type = lib.types.listOf lib.types.str;
       };
 
-      host = mkOption {
-        default = "127.0.0.1";
-        description = lib.mdDoc "The host-ip to bind to.";
-        type = types.str;
+      host = lib.mkOption {
+        default = "localhost";
+        description = lib.mdDoc ''
+          The host name or IP address the server should listen to.
+        '';
+        type = lib.types.str;
       };
 
-      port = mkOption {
+      port = lib.mkOption {
         default = 4444;
-        description = lib.mdDoc "The port where code-server runs.";
-        type = types.port;
+        description = lib.mdDoc ''
+          The port the server should listen to.
+        '';
+        type = lib.types.port;
       };
 
-      auth = mkOption {
+      auth = lib.mkOption {
         default = "password";
-        description = lib.mdDoc "The type of authentication to use.";
-        type = types.enum [ "none" "password" ];
+        description = lib.mdDoc ''
+          The type of authentication to use.
+        '';
+        type = lib.types.enum [ "none" "password" ];
       };
 
-      hashedPassword = mkOption {
+      hashedPassword = lib.mkOption {
         default = "";
-        description =
-          lib.mdDoc "Create the password with: `echo -n 'thisismypassword' | npx argon2-cli -e`.";
-        type = types.str;
+        description = lib.mdDoc ''
+          Create the password with: `echo -n 'thisismypassword' | npx argon2-cli -e`.
+        '';
+        type = lib.types.str;
       };
 
-      user = mkOption {
+      user = lib.mkOption {
         default = defaultUser;
         example = "yourUser";
         description = lib.mdDoc ''
           The user to run code-server as.
           By default, a user named `${defaultUser}` will be created.
         '';
-        type = types.str;
+        type = lib.types.str;
       };
 
-      group = mkOption {
+      group = lib.mkOption {
         default = defaultGroup;
         example = "yourGroup";
         description = lib.mdDoc ''
           The group to run code-server under.
           By default, a group named `${defaultGroup}` will be created.
         '';
-        type = types.str;
+        type = lib.types.str;
       };
 
-      extraGroups = mkOption {
+      extraGroups = lib.mkOption {
         default = [ ];
-        description =
-          lib.mdDoc "An array of additional groups for the `${defaultUser}` user.";
+        description = lib.mdDoc ''
+          An array of additional groups for the `${defaultUser}` user.
+        '';
         example = [ "docker" ];
-        type = types.listOf types.str;
+        type = lib.types.listOf lib.types.str;
+      };
+
+      socket = lib.mkOption {
+        default = null;
+        example = "/run/code-server/socket";
+        description = lib.mdDoc ''
+          Path to a socket (bind-addr will be ignored).
+        '';
+        type = lib.types.nullOr lib.types.str;
+      };
+
+      socketMode = lib.mkOption {
+        default = null;
+        description = lib.mdDoc ''
+           File mode of the socket.
+        '';
+        type = lib.types.nullOr lib.types.str;
+      };
+
+      userDataDir = lib.mkOption {
+        default = null;
+        description = lib.mdDoc ''
+          Path to the user data directory.
+        '';
+        type = lib.types.nullOr lib.types.str;
+      };
+
+      extensionsDir = lib.mkOption {
+        default = null;
+        description = lib.mdDoc ''
+          Path to the extensions directory.
+        '';
+        type = lib.types.nullOr lib.types.str;
+      };
+
+      proxyDomain = lib.mkOption {
+        default = null;
+        example = "code-server.lan";
+        description = lib.mdDoc ''
+          Domain used for proxying ports.
+        '';
+        type = lib.types.nullOr lib.types.str;
+      };
+
+      disableTelemetry = lib.mkOption {
+        default = false;
+        example = true;
+        description = lib.mdDoc ''
+          Disable telemetry.
+        '';
+        type = lib.types.bool;
+      };
+
+      disableUpdateCheck = lib.mkOption {
+        default = false;
+        example = true;
+        description = lib.mdDoc ''
+          Disable update check.
+          Without this flag, code-server checks every 6 hours against the latest github release and
+          then notifies you once every week that a new release is available.
+        '';
+        type = lib.types.bool;
+      };
+
+      disableFileDownloads = lib.mkOption {
+        default = false;
+        example = true;
+        description = lib.mdDoc ''
+          Disable file downloads from Code.
+        '';
+        type = lib.types.bool;
+      };
+
+      disableWorkspaceTrust = lib.mkOption {
+        default = false;
+        example = true;
+        description = lib.mdDoc ''
+          Disable Workspace Trust feature.
+        '';
+        type = lib.types.bool;
+      };
+
+      disableGettingStartedOverride = lib.mkOption {
+        default = false;
+        example = true;
+        description = lib.mdDoc ''
+          Disable the coder/coder override in the Help: Getting Started page.
+        '';
+        type = lib.types.bool;
       };
 
     };
   };
 
-  ###### implementation
-  config = mkIf cfg.enable {
+  config = lib.mkIf cfg.enable {
     systemd.services.code-server = {
-      description = "VSCode server";
+      description = "Code server";
       wantedBy = [ "multi-user.target" ];
       after = [ "network-online.target" ];
       path = cfg.extraPackages;
@@ -109,18 +211,37 @@ in {
         HASHED_PASSWORD = cfg.hashedPassword;
       } // cfg.extraEnvironment;
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/code-server --bind-addr ${cfg.host}:${toString cfg.port} --auth ${cfg.auth} " + lib.escapeShellArgs cfg.extraArguments;
+        ExecStart = ''
+          ${lib.getExe cfg.package} \
+            --auth=${cfg.auth} \
+            --bind-addr=${cfg.host}:${toString cfg.port} \
+          '' + lib.optionalString (cfg.socket != null) ''
+            --socket=${cfg.socket} \
+          '' + lib.optionalString (cfg.userDataDir != null) ''
+            --user-data-dir=${cfg.userDataDir} \
+          '' + lib.optionalString (cfg.extensionsDir != null) ''
+            --extensions-dir=${cfg.extensionsDir} \
+          '' + lib.optionalString (cfg.disableTelemetry == true) ''
+            --disable-telemetry \
+          '' + lib.optionalString (cfg.disableUpdateCheck == true) ''
+            --disable-update-check \
+          '' + lib.optionalString (cfg.disableFileDownloads == true) ''
+            --disable-file-downloads \
+          '' + lib.optionalString (cfg.disableWorkspaceTrust == true) ''
+            --disable-workspace-trust \
+          '' + lib.optionalString (cfg.disableGettingStartedOverride == true) ''
+            --disable-getting-started-override \
+          '' + lib.escapeShellArgs cfg.extraArguments;
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         RuntimeDirectory = cfg.user;
         User = cfg.user;
         Group = cfg.group;
         Restart = "on-failure";
       };
-
     };
 
-    users.users."${cfg.user}" = mkMerge [
-      (mkIf (cfg.user == defaultUser) {
+    users.users."${cfg.user}" = lib.mkMerge [
+      (lib.mkIf (cfg.user == defaultUser) {
         isNormalUser = true;
         description = "code-server user";
         inherit (cfg) group;
@@ -131,9 +252,8 @@ in {
       }
     ];
 
-    users.groups."${defaultGroup}" = mkIf (cfg.group == defaultGroup) { };
-
+    users.groups."${defaultGroup}" = lib.mkIf (cfg.group == defaultGroup) { };
   };
 
-  meta.maintainers = with maintainers; [ stackshadow ];
+  meta.maintainers = [ lib.maintainers.stackshadow ];
 }
diff --git a/nixos/modules/services/web-apps/coder.nix b/nixos/modules/services/web-apps/coder.nix
new file mode 100644
index 00000000000..469a29bc3aa
--- /dev/null
+++ b/nixos/modules/services/web-apps/coder.nix
@@ -0,0 +1,217 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.coder;
+  name = "coder";
+in {
+  options = {
+    services.coder = {
+      enable = mkEnableOption (lib.mdDoc "Coder service");
+
+      user = mkOption {
+        type = types.str;
+        default = "coder";
+        description = lib.mdDoc ''
+          User under which the coder service runs.
+
+          ::: {.note}
+          If left as the default value this user will automatically be created
+          on system activation, otherwise it needs to be configured manually.
+          :::
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "coder";
+        description = lib.mdDoc ''
+          Group under which the coder service runs.
+
+          ::: {.note}
+          If left as the default value this group will automatically be created
+          on system activation, otherwise it needs to be configured manually.
+          :::
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.coder;
+        description = lib.mdDoc ''
+          Package to use for the service.
+        '';
+        defaultText = literalExpression "pkgs.coder";
+      };
+
+      homeDir = mkOption {
+        type = types.str;
+        description = lib.mdDoc ''
+          Home directory for coder user.
+        '';
+        default = "/var/lib/coder";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        description = lib.mdDoc ''
+          Listen address.
+        '';
+        default = "127.0.0.1:3000";
+      };
+
+      accessUrl = mkOption {
+        type = types.nullOr types.str;
+        description = lib.mdDoc ''
+          Access URL should be a external IP address or domain with DNS records pointing to Coder.
+        '';
+        default = null;
+        example = "https://coder.example.com";
+      };
+
+      wildcardAccessUrl = mkOption {
+        type = types.nullOr types.str;
+        description = lib.mdDoc ''
+          If you are providing TLS certificates directly to the Coder server, you must use a single certificate for the root and wildcard domains.
+        '';
+        default = null;
+        example = "*.coder.example.com";
+      };
+
+      database = {
+        createLocally = mkOption {
+          type = types.bool;
+          default = true;
+          description = lib.mdDoc ''
+            Create the database and database user locally.
+          '';
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "/run/postgresql";
+          description = lib.mdDoc ''
+            Hostname hosting the database.
+          '';
+        };
+
+        database = mkOption {
+          type = types.str;
+          default = "coder";
+          description = lib.mdDoc ''
+            Name of database.
+          '';
+        };
+
+        username = mkOption {
+          type = types.str;
+          default = "coder";
+          description = lib.mdDoc ''
+            Username for accessing the database.
+          '';
+        };
+
+        password = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = lib.mdDoc ''
+            Password for accessing the database.
+          '';
+        };
+
+        sslmode = mkOption {
+          type = types.nullOr types.str;
+          default = "disable";
+          description = lib.mdDoc ''
+            Password for accessing the database.
+          '';
+        };
+      };
+
+      tlsCert = mkOption {
+        type = types.nullOr types.path;
+        description = lib.mdDoc ''
+          The path to the TLS certificate.
+        '';
+        default = null;
+      };
+
+      tlsKey = mkOption {
+        type = types.nullOr types.path;
+        description = lib.mdDoc ''
+          The path to the TLS key.
+        '';
+        default = null;
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.database.createLocally -> cfg.database.username == name;
+        message = "services.coder.database.username must be set to ${user} if services.coder.database.createLocally is set true";
+      }
+    ];
+
+    systemd.services.coder = {
+      description = "Coder - Self-hosted developer workspaces on your infra";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        CODER_ACCESS_URL = cfg.accessUrl;
+        CODER_WILDCARD_ACCESS_URL = cfg.wildcardAccessUrl;
+        CODER_PG_CONNECTION_URL = "user=${cfg.database.username} ${optionalString (cfg.database.password != null) "password=${cfg.database.password}"} database=${cfg.database.database} host=${cfg.database.host} ${optionalString (cfg.database.sslmode != null) "sslmode=${cfg.database.sslmode}"}";
+        CODER_ADDRESS = cfg.listenAddress;
+        CODER_TLS_ENABLE = optionalString (cfg.tlsCert != null) "1";
+        CODER_TLS_CERT_FILE = cfg.tlsCert;
+        CODER_TLS_KEY_FILE = cfg.tlsKey;
+      };
+
+      serviceConfig = {
+        ProtectSystem = "full";
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+        SecureBits = "keep-caps";
+        AmbientCapabilities = "CAP_IPC_LOCK CAP_NET_BIND_SERVICE";
+        CacheDirectory = "coder";
+        CapabilityBoundingSet = "CAP_SYSLOG CAP_IPC_LOCK CAP_NET_BIND_SERVICE";
+        KillSignal = "SIGINT";
+        KillMode = "mixed";
+        NoNewPrivileges = "yes";
+        Restart = "on-failure";
+        ExecStart = "${cfg.package}/bin/coder server";
+        User = cfg.user;
+        Group = cfg.group;
+      };
+    };
+
+    services.postgresql = lib.mkIf cfg.database.createLocally {
+      enable = true;
+      ensureDatabases = [
+        cfg.database.database
+      ];
+      ensureUsers = [{
+        name = cfg.database.username;
+        ensurePermissions = {
+          "DATABASE \"${cfg.database.database}\"" = "ALL PRIVILEGES";
+        };
+        }
+      ];
+    };
+
+    users.groups = optionalAttrs (cfg.group == name) {
+      "${cfg.group}" = {};
+    };
+    users.users = optionalAttrs (cfg.user == name) {
+      ${name} = {
+        description = "Coder service user";
+        group = cfg.group;
+        home = cfg.homeDir;
+        createHome = true;
+        isSystemUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/dex.nix b/nixos/modules/services/web-apps/dex.nix
index f69f1749aeb..bd041db007a 100644
--- a/nixos/modules/services/web-apps/dex.nix
+++ b/nixos/modules/services/web-apps/dex.nix
@@ -6,7 +6,7 @@ let
   cfg = config.services.dex;
   fixClient = client: if client ? secretFile then ((builtins.removeAttrs client [ "secretFile" ]) // { secret = client.secretFile; }) else client;
   filteredSettings = mapAttrs (n: v: if n == "staticClients" then (builtins.map fixClient v) else v) cfg.settings;
-  secretFiles = flatten (builtins.map (c: if c ? secretFile then [ c.secretFile ] else []) (cfg.settings.staticClients or []));
+  secretFiles = flatten (builtins.map (c: optional (c ? secretFile) c.secretFile) (cfg.settings.staticClients or []));
 
   settingsFormat = pkgs.formats.yaml {};
   configFile = settingsFormat.generate "config.yaml" filteredSettings;
diff --git a/nixos/modules/services/web-apps/discourse.md b/nixos/modules/services/web-apps/discourse.md
new file mode 100644
index 00000000000..35180bea87d
--- /dev/null
+++ b/nixos/modules/services/web-apps/discourse.md
@@ -0,0 +1,286 @@
+# Discourse {#module-services-discourse}
+
+[Discourse](https://www.discourse.org/) is a
+modern and open source discussion platform.
+
+## Basic usage {#module-services-discourse-basic-usage}
+
+A minimal configuration using Let's Encrypt for TLS certificates looks like this:
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+security.acme.email = "me@example.com";
+security.acme.acceptTerms = true;
+```
+
+Provided a proper DNS setup, you'll be able to connect to the
+instance at `discourse.example.com` and log in
+using the credentials provided in
+`services.discourse.admin`.
+
+## Using a regular TLS certificate {#module-services-discourse-tls}
+
+To set up TLS using a regular certificate and key on file, use
+the [](#opt-services.discourse.sslCertificate)
+and [](#opt-services.discourse.sslCertificateKey)
+options:
+
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  sslCertificate = "/path/to/ssl_certificate";
+  sslCertificateKey = "/path/to/ssl_certificate_key";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+```
+
+## Database access {#module-services-discourse-database}
+
+Discourse uses PostgreSQL to store most of its
+data. A database will automatically be enabled and a database
+and role created unless [](#opt-services.discourse.database.host) is changed from
+its default of `null` or [](#opt-services.discourse.database.createLocally) is set
+to `false`.
+
+External database access can also be configured by setting
+[](#opt-services.discourse.database.host),
+[](#opt-services.discourse.database.username) and
+[](#opt-services.discourse.database.passwordFile) as
+appropriate. Note that you need to manually create a database
+called `discourse` (or the name you chose in
+[](#opt-services.discourse.database.name)) and
+allow the configured database user full access to it.
+
+## Email {#module-services-discourse-mail}
+
+In addition to the basic setup, you'll want to configure an SMTP
+server Discourse can use to send user
+registration and password reset emails, among others. You can
+also optionally let Discourse receive
+email, which enables people to reply to threads and conversations
+via email.
+
+A basic setup which assumes you want to use your configured
+[hostname](#opt-services.discourse.hostname) as
+email domain can be done like this:
+
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  sslCertificate = "/path/to/ssl_certificate";
+  sslCertificateKey = "/path/to/ssl_certificate_key";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  mail.outgoing = {
+    serverAddress = "smtp.emailprovider.com";
+    port = 587;
+    username = "user@emailprovider.com";
+    passwordFile = "/path/to/smtp_password_file";
+  };
+  mail.incoming.enable = true;
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+```
+
+This assumes you have set up an MX record for the address you've
+set in [hostname](#opt-services.discourse.hostname) and
+requires proper SPF, DKIM and DMARC configuration to be done for
+the domain you're sending from, in order for email to be reliably delivered.
+
+If you want to use a different domain for your outgoing email
+(for example `example.com` instead of
+`discourse.example.com`) you should set
+[](#opt-services.discourse.mail.notificationEmailAddress) and
+[](#opt-services.discourse.mail.contactEmailAddress) manually.
+
+::: {.note}
+Setup of TLS for incoming email is currently only configured
+automatically when a regular TLS certificate is used, i.e. when
+[](#opt-services.discourse.sslCertificate) and
+[](#opt-services.discourse.sslCertificateKey) are
+set.
+:::
+
+## Additional settings {#module-services-discourse-settings}
+
+Additional site settings and backend settings, for which no
+explicit NixOS options are provided,
+can be set in [](#opt-services.discourse.siteSettings) and
+[](#opt-services.discourse.backendSettings) respectively.
+
+### Site settings {#module-services-discourse-site-settings}
+
+"Site settings" are the settings that can be
+changed through the Discourse
+UI. Their *default* values can be set using
+[](#opt-services.discourse.siteSettings).
+
+Settings are expressed as a Nix attribute set which matches the
+structure of the configuration in
+[config/site_settings.yml](https://github.com/discourse/discourse/blob/master/config/site_settings.yml).
+To find a setting's path, you only need to care about the first
+two levels; i.e. its category (e.g. `login`)
+and name (e.g. `invite_only`).
+
+Settings containing secret data should be set to an attribute
+set containing the attribute `_secret` - a
+string pointing to a file containing the value the option
+should be set to. See the example.
+
+### Backend settings {#module-services-discourse-backend-settings}
+
+Settings are expressed as a Nix attribute set which matches the
+structure of the configuration in
+[config/discourse.conf](https://github.com/discourse/discourse/blob/stable/config/discourse_defaults.conf).
+Empty parameters can be defined by setting them to
+`null`.
+
+### Example {#module-services-discourse-settings-example}
+
+The following example sets the title and description of the
+Discourse instance and enables
+GitHub login in the site settings,
+and changes a few request limits in the backend settings:
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  sslCertificate = "/path/to/ssl_certificate";
+  sslCertificateKey = "/path/to/ssl_certificate_key";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  mail.outgoing = {
+    serverAddress = "smtp.emailprovider.com";
+    port = 587;
+    username = "user@emailprovider.com";
+    passwordFile = "/path/to/smtp_password_file";
+  };
+  mail.incoming.enable = true;
+  siteSettings = {
+    required = {
+      title = "My Cats";
+      site_description = "Discuss My Cats (and be nice plz)";
+    };
+    login = {
+      enable_github_logins = true;
+      github_client_id = "a2f6dfe838cb3206ce20";
+      github_client_secret._secret = /run/keys/discourse_github_client_secret;
+    };
+  };
+  backendSettings = {
+    max_reqs_per_ip_per_minute = 300;
+    max_reqs_per_ip_per_10_seconds = 60;
+    max_asset_reqs_per_ip_per_10_seconds = 250;
+    max_reqs_per_ip_mode = "warn+block";
+  };
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+```
+
+In the resulting site settings file, the
+`login.github_client_secret` key will be set
+to the contents of the
+{file}`/run/keys/discourse_github_client_secret`
+file.
+
+## Plugins {#module-services-discourse-plugins}
+
+You can install Discourse plugins
+using the [](#opt-services.discourse.plugins)
+option. Pre-packaged plugins are provided in
+`<your_discourse_package_here>.plugins`. If
+you want the full suite of plugins provided through
+`nixpkgs`, you can also set the [](#opt-services.discourse.package) option to
+`pkgs.discourseAllPlugins`.
+
+Plugins can be built with the
+`<your_discourse_package_here>.mkDiscoursePlugin`
+function. Normally, it should suffice to provide a
+`name` and `src` attribute. If
+the plugin has Ruby dependencies, however, they need to be
+packaged in accordance with the [Developing with Ruby](https://nixos.org/manual/nixpkgs/stable/#developing-with-ruby)
+section of the Nixpkgs manual and the
+appropriate gem options set in `bundlerEnvArgs`
+(normally `gemdir` is sufficient). A plugin's
+Ruby dependencies are listed in its
+{file}`plugin.rb` file as function calls to
+`gem`. To construct the corresponding
+{file}`Gemfile` manually, run {command}`bundle init`, then add the `gem` lines to it
+verbatim.
+
+Much of the packaging can be done automatically by the
+{file}`nixpkgs/pkgs/servers/web-apps/discourse/update.py`
+script - just add the plugin to the `plugins`
+list in the `update_plugins` function and run
+the script:
+```bash
+./update.py update-plugins
+```
+
+Some plugins provide [site settings](#module-services-discourse-site-settings).
+Their defaults can be configured using [](#opt-services.discourse.siteSettings), just like
+regular site settings. To find the names of these settings, look
+in the `config/settings.yml` file of the plugin
+repo.
+
+For example, to add the [discourse-spoiler-alert](https://github.com/discourse/discourse-spoiler-alert)
+and [discourse-solved](https://github.com/discourse/discourse-solved)
+plugins, and disable `discourse-spoiler-alert`
+by default:
+
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  sslCertificate = "/path/to/ssl_certificate";
+  sslCertificateKey = "/path/to/ssl_certificate_key";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  mail.outgoing = {
+    serverAddress = "smtp.emailprovider.com";
+    port = 587;
+    username = "user@emailprovider.com";
+    passwordFile = "/path/to/smtp_password_file";
+  };
+  mail.incoming.enable = true;
+  plugins = with config.services.discourse.package.plugins; [
+    discourse-spoiler-alert
+    discourse-solved
+  ];
+  siteSettings = {
+    plugins = {
+      spoiler_enabled = false;
+    };
+  };
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+```
diff --git a/nixos/modules/services/web-apps/discourse.nix b/nixos/modules/services/web-apps/discourse.nix
index b8104ade467..f80eb6b4c7f 100644
--- a/nixos/modules/services/web-apps/discourse.nix
+++ b/nixos/modules/services/web-apps/discourse.nix
@@ -615,6 +615,7 @@ in
       s3_endpoint = null;
       s3_http_continue_timeout = null;
       s3_install_cors_rule = null;
+      s3_asset_cdn_url = null;
 
       max_user_api_reqs_per_minute = 20;
       max_user_api_reqs_per_day = 2880;
@@ -647,6 +648,12 @@ in
       multisite_config_path = "config/multisite.yml";
       enable_long_polling = null;
       long_polling_interval = null;
+      preload_link_header = false;
+      redirect_avatar_requests = false;
+      pg_force_readonly_mode = false;
+      dns_query_timeout_secs = null;
+      regex_timeout_seconds = 2;
+      allow_impersonation = true;
     };
 
     services.redis.servers.discourse =
@@ -1011,6 +1018,7 @@ in
         notification_email = cfg.mail.notificationEmailAddress;
         contact_email = cfg.mail.contactEmailAddress;
       };
+      security.force_https = tlsEnabled;
       email = {
         manual_polling_enabled = cfg.mail.incoming.enable;
         reply_by_email_enabled = cfg.mail.incoming.enable;
@@ -1020,8 +1028,8 @@ in
 
     services.postfix = lib.mkIf cfg.mail.incoming.enable {
       enable = true;
-      sslCert = if cfg.sslCertificate != null then cfg.sslCertificate else "";
-      sslKey = if cfg.sslCertificateKey != null then cfg.sslCertificateKey else "";
+      sslCert = lib.optionalString (cfg.sslCertificate != null) cfg.sslCertificate;
+      sslKey = lib.optionalString (cfg.sslCertificateKey != null) cfg.sslCertificateKey;
 
       origin = cfg.hostname;
       relayDomains = [ cfg.hostname ];
@@ -1080,6 +1088,6 @@ in
     ];
   };
 
-  meta.doc = ./discourse.xml;
+  meta.doc = ./discourse.md;
   meta.maintainers = [ lib.maintainers.talyz ];
 }
diff --git a/nixos/modules/services/web-apps/discourse.xml b/nixos/modules/services/web-apps/discourse.xml
deleted file mode 100644
index ad9b65abf51..00000000000
--- a/nixos/modules/services/web-apps/discourse.xml
+++ /dev/null
@@ -1,355 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-discourse">
- <title>Discourse</title>
- <para>
-   <link xlink:href="https://www.discourse.org/">Discourse</link> is a
-   modern and open source discussion platform.
- </para>
-
- <section xml:id="module-services-discourse-basic-usage">
-   <title>Basic usage</title>
-   <para>
-     A minimal configuration using Let's Encrypt for TLS certificates looks like this:
-<programlisting>
-services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
-  };
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
-};
-<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
-<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
-</programlisting>
-   </para>
-
-   <para>
-     Provided a proper DNS setup, you'll be able to connect to the
-     instance at <literal>discourse.example.com</literal> and log in
-     using the credentials provided in
-     <literal>services.discourse.admin</literal>.
-   </para>
- </section>
-
- <section xml:id="module-services-discourse-tls">
-   <title>Using a regular TLS certificate</title>
-   <para>
-     To set up TLS using a regular certificate and key on file, use
-     the <xref linkend="opt-services.discourse.sslCertificate" />
-     and <xref linkend="opt-services.discourse.sslCertificateKey" />
-     options:
-
-<programlisting>
-services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
-  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
-  admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
-  };
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
-};
-</programlisting>
-
-   </para>
- </section>
-
- <section xml:id="module-services-discourse-database">
-   <title>Database access</title>
-   <para>
-     <productname>Discourse</productname> uses
-     <productname>PostgreSQL</productname> to store most of its
-     data. A database will automatically be enabled and a database
-     and role created unless <xref
-     linkend="opt-services.discourse.database.host" /> is changed from
-     its default of <literal>null</literal> or <xref
-     linkend="opt-services.discourse.database.createLocally" /> is set
-     to <literal>false</literal>.
-   </para>
-
-   <para>
-     External database access can also be configured by setting
-     <xref linkend="opt-services.discourse.database.host" />, <xref
-     linkend="opt-services.discourse.database.username" /> and <xref
-     linkend="opt-services.discourse.database.passwordFile" /> as
-     appropriate. Note that you need to manually create a database
-     called <literal>discourse</literal> (or the name you chose in
-     <xref linkend="opt-services.discourse.database.name" />) and
-     allow the configured database user full access to it.
-   </para>
- </section>
-
- <section xml:id="module-services-discourse-mail">
-   <title>Email</title>
-   <para>
-     In addition to the basic setup, you'll want to configure an SMTP
-     server <productname>Discourse</productname> can use to send user
-     registration and password reset emails, among others. You can
-     also optionally let <productname>Discourse</productname> receive
-     email, which enables people to reply to threads and conversations
-     via email.
-   </para>
-
-   <para>
-     A basic setup which assumes you want to use your configured <link
-     linkend="opt-services.discourse.hostname">hostname</link> as
-     email domain can be done like this:
-
-<programlisting>
-services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
-  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
-  admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
-  };
-  mail.outgoing = {
-    <link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
-    <link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
-  };
-  <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
-};
-</programlisting>
-
-     This assumes you have set up an MX record for the address you've
-     set in <link linkend="opt-services.discourse.hostname">hostname</link> and
-     requires proper SPF, DKIM and DMARC configuration to be done for
-     the domain you're sending from, in order for email to be reliably delivered.
-   </para>
-
-   <para>
-     If you want to use a different domain for your outgoing email
-     (for example <literal>example.com</literal> instead of
-     <literal>discourse.example.com</literal>) you should set
-     <xref linkend="opt-services.discourse.mail.notificationEmailAddress" /> and
-     <xref linkend="opt-services.discourse.mail.contactEmailAddress" /> manually.
-   </para>
-
-   <note>
-     <para>
-       Setup of TLS for incoming email is currently only configured
-       automatically when a regular TLS certificate is used, i.e. when
-       <xref linkend="opt-services.discourse.sslCertificate" /> and
-       <xref linkend="opt-services.discourse.sslCertificateKey" /> are
-       set.
-     </para>
-   </note>
-
- </section>
-
- <section xml:id="module-services-discourse-settings">
-   <title>Additional settings</title>
-   <para>
-     Additional site settings and backend settings, for which no
-     explicit <productname>NixOS</productname> options are provided,
-     can be set in <xref linkend="opt-services.discourse.siteSettings" /> and
-     <xref linkend="opt-services.discourse.backendSettings" /> respectively.
-   </para>
-
-   <section xml:id="module-services-discourse-site-settings">
-     <title>Site settings</title>
-     <para>
-       <quote>Site settings</quote> are the settings that can be
-       changed through the <productname>Discourse</productname>
-       UI. Their <emphasis>default</emphasis> values can be set using
-       <xref linkend="opt-services.discourse.siteSettings" />.
-     </para>
-
-     <para>
-       Settings are expressed as a Nix attribute set which matches the
-       structure of the configuration in
-       <link xlink:href="https://github.com/discourse/discourse/blob/master/config/site_settings.yml">config/site_settings.yml</link>.
-       To find a setting's path, you only need to care about the first
-       two levels; i.e. its category (e.g. <literal>login</literal>)
-       and name (e.g. <literal>invite_only</literal>).
-     </para>
-
-     <para>
-       Settings containing secret data should be set to an attribute
-       set containing the attribute <literal>_secret</literal> - a
-       string pointing to a file containing the value the option
-       should be set to. See the example.
-     </para>
-   </section>
-
-   <section xml:id="module-services-discourse-backend-settings">
-     <title>Backend settings</title>
-     <para>
-       Settings are expressed as a Nix attribute set which matches the
-       structure of the configuration in
-       <link xlink:href="https://github.com/discourse/discourse/blob/stable/config/discourse_defaults.conf">config/discourse.conf</link>.
-       Empty parameters can be defined by setting them to
-       <literal>null</literal>.
-     </para>
-   </section>
-
-   <section xml:id="module-services-discourse-settings-example">
-     <title>Example</title>
-     <para>
-       The following example sets the title and description of the
-       <productname>Discourse</productname> instance and enables
-       <productname>GitHub</productname> login in the site settings,
-       and changes a few request limits in the backend settings:
-<programlisting>
-services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
-  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
-  admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
-  };
-  mail.outgoing = {
-    <link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
-    <link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
-  };
-  <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
-  <link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
-    required = {
-      title = "My Cats";
-      site_description = "Discuss My Cats (and be nice plz)";
-    };
-    login = {
-      enable_github_logins = true;
-      github_client_id = "a2f6dfe838cb3206ce20";
-      github_client_secret._secret = /run/keys/discourse_github_client_secret;
-    };
-  };
-  <link linkend="opt-services.discourse.backendSettings">backendSettings</link> = {
-    max_reqs_per_ip_per_minute = 300;
-    max_reqs_per_ip_per_10_seconds = 60;
-    max_asset_reqs_per_ip_per_10_seconds = 250;
-    max_reqs_per_ip_mode = "warn+block";
-  };
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
-};
-</programlisting>
-     </para>
-     <para>
-       In the resulting site settings file, the
-       <literal>login.github_client_secret</literal> key will be set
-       to the contents of the
-       <filename>/run/keys/discourse_github_client_secret</filename>
-       file.
-     </para>
-   </section>
- </section>
-  <section xml:id="module-services-discourse-plugins">
-    <title>Plugins</title>
-    <para>
-      You can install <productname>Discourse</productname> plugins
-      using the <xref linkend="opt-services.discourse.plugins" />
-      option. Pre-packaged plugins are provided in
-      <literal>&lt;your_discourse_package_here&gt;.plugins</literal>. If
-      you want the full suite of plugins provided through
-      <literal>nixpkgs</literal>, you can also set the <xref
-      linkend="opt-services.discourse.package" /> option to
-      <literal>pkgs.discourseAllPlugins</literal>.
-    </para>
-
-    <para>
-      Plugins can be built with the
-      <literal>&lt;your_discourse_package_here&gt;.mkDiscoursePlugin</literal>
-      function. Normally, it should suffice to provide a
-      <literal>name</literal> and <literal>src</literal> attribute. If
-      the plugin has Ruby dependencies, however, they need to be
-      packaged in accordance with the <link
-      xlink:href="https://nixos.org/manual/nixpkgs/stable/#developing-with-ruby">Developing
-      with Ruby</link> section of the Nixpkgs manual and the
-      appropriate gem options set in <literal>bundlerEnvArgs</literal>
-      (normally <literal>gemdir</literal> is sufficient). A plugin's
-      Ruby dependencies are listed in its
-      <filename>plugin.rb</filename> file as function calls to
-      <literal>gem</literal>. To construct the corresponding
-      <filename>Gemfile</filename> manually, run <command>bundle
-      init</command>, then add the <literal>gem</literal> lines to it
-      verbatim.
-    </para>
-
-    <para>
-      Much of the packaging can be done automatically by the
-      <filename>nixpkgs/pkgs/servers/web-apps/discourse/update.py</filename>
-      script - just add the plugin to the <literal>plugins</literal>
-      list in the <function>update_plugins</function> function and run
-      the script:
-      <programlisting language="bash">
-./update.py update-plugins
-</programlisting>
-    </para>
-
-    <para>
-      Some plugins provide <link
-      linkend="module-services-discourse-site-settings">site
-      settings</link>. Their defaults can be configured using <xref
-      linkend="opt-services.discourse.siteSettings" />, just like
-      regular site settings. To find the names of these settings, look
-      in the <literal>config/settings.yml</literal> file of the plugin
-      repo.
-    </para>
-
-    <para>
-      For example, to add the <link
-      xlink:href="https://github.com/discourse/discourse-spoiler-alert">discourse-spoiler-alert</link>
-      and <link
-      xlink:href="https://github.com/discourse/discourse-solved">discourse-solved</link>
-      plugins, and disable <literal>discourse-spoiler-alert</literal>
-      by default:
-
-<programlisting>
-services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
-  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
-  admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
-  };
-  mail.outgoing = {
-    <link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
-    <link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
-  };
-  <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
-  <link linkend="opt-services.discourse.mail.incoming.enable">plugins</link> = with config.services.discourse.package.plugins; [
-    discourse-spoiler-alert
-    discourse-solved
-  ];
-  <link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
-    plugins = {
-      spoiler_enabled = false;
-    };
-  };
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
-};
-</programlisting>
-
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
index f0b3c7b2bcf..9e9bfb1bfd8 100644
--- a/nixos/modules/services/web-apps/dokuwiki.nix
+++ b/nixos/modules/services/web-apps/dokuwiki.nix
@@ -3,63 +3,184 @@
 with lib;
 
 let
+  inherit (lib.options) showOption showFiles;
+
   cfg = config.services.dokuwiki;
   eachSite = cfg.sites;
   user = "dokuwiki";
   webserver = config.services.${cfg.webserver};
 
-  dokuwikiAclAuthConfig = hostName: cfg: pkgs.writeText "acl.auth-${hostName}.php" ''
+  mkPhpIni = generators.toKeyValue {
+    mkKeyValue = generators.mkKeyValueDefault {} " = ";
+  };
+  mkPhpPackage = cfg: cfg.phpPackage.buildEnv {
+    extraConfig = mkPhpIni cfg.phpOptions;
+  };
+
+  dokuwikiAclAuthConfig = hostName: cfg: let
+    inherit (cfg) acl;
+    acl_gen = concatMapStringsSep "\n" (l: "${l.page} \t ${l.actor} \t ${toString l.level}");
+  in pkgs.writeText "acl.auth-${hostName}.php" ''
     # acl.auth.php
     # <?php exit()?>
     #
     # Access Control Lists
     #
-    ${toString cfg.acl}
+    ${if isString acl then acl else acl_gen acl}
   '';
 
-  dokuwikiLocalConfig = hostName: cfg: pkgs.writeText "local-${hostName}.php" ''
-    <?php
-    $conf['savedir'] = '${cfg.stateDir}';
-    $conf['superuser'] = '${toString cfg.superUser}';
-    $conf['useacl'] = '${toString cfg.aclUse}';
-    $conf['disableactions'] = '${cfg.disableActions}';
-    ${toString cfg.extraConfig}
+  mergeConfig = cfg: {
+    useacl = false; # Dokuwiki default
+    savedir = cfg.stateDir;
+  } // cfg.settings;
+
+  writePhpFile = name: text: pkgs.writeTextFile {
+    inherit name;
+    text = "<?php\n${text}";
+    checkPhase = "${pkgs.php81}/bin/php --syntax-check $target";
+  };
+
+  mkPhpValue = v: let
+    isHasAttr = s: isAttrs v && hasAttr s v;
+  in
+    if isString v then escapeShellArg v
+    # NOTE: If any value contains a , (comma) this will not get escaped
+    else if isList v && any lib.strings.isCoercibleToString v then escapeShellArg (concatMapStringsSep "," toString v)
+    else if isInt v then toString v
+    else if isBool v then toString (if v then 1 else 0)
+    else if isHasAttr "_file" then "trim(file_get_contents(${lib.escapeShellArg v._file}))"
+    else if isHasAttr "_raw" then v._raw
+    else abort "The dokuwiki localConf value ${lib.generators.toPretty {} v} can not be encoded."
+  ;
+
+  mkPhpAttrVals = v: flatten (mapAttrsToList mkPhpKeyVal v);
+  mkPhpKeyVal = k: v: let
+    values = if (isAttrs v && (hasAttr "_file" v || hasAttr "_raw" v )) || !isAttrs v then
+      [" = ${mkPhpValue v};"]
+    else
+      mkPhpAttrVals v;
+  in map (e: "[${escapeShellArg k}]${e}") (flatten values);
+
+  dokuwikiLocalConfig = hostName: cfg: let
+    conf_gen = c: map (v: "$conf${v}") (mkPhpAttrVals c);
+  in writePhpFile "local-${hostName}.php" ''
+    ${concatStringsSep "\n" (conf_gen cfg.mergedConfig)}
   '';
 
-  dokuwikiPluginsLocalConfig = hostName: cfg: pkgs.writeText "plugins.local-${hostName}.php" ''
-    <?php
-    ${cfg.pluginsConfig}
+  dokuwikiPluginsLocalConfig = hostName: cfg: let
+    pc = cfg.pluginsConfig;
+    pc_gen = pc: concatStringsSep "\n" (mapAttrsToList (n: v: "$plugins['${n}'] = ${boolToString v};") pc);
+  in writePhpFile "plugins.local-${hostName}.php" ''
+    ${if isString pc then pc else pc_gen pc}
   '';
 
 
-  pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
-    pname = "dokuwiki-${hostName}";
-    version = src.version;
-    src = cfg.package;
+  pkg = hostName: cfg: cfg.package.combine {
+    inherit (cfg) plugins templates;
 
-    installPhase = ''
-      mkdir -p $out
-      cp -r * $out/
+    pname = p: "${p.pname}-${hostName}";
+
+    basePackage = cfg.package;
+    localConfig = dokuwikiLocalConfig hostName cfg;
+    pluginsConfig = dokuwikiPluginsLocalConfig hostName cfg;
+    aclConfig = if cfg.settings.useacl && cfg.acl != null then dokuwikiAclAuthConfig hostName cfg else null;
+  };
 
-      # symlink the dokuwiki config
-      ln -s ${dokuwikiLocalConfig hostName cfg} $out/share/dokuwiki/local.php
+  aclOpts = { ... }: {
+    options = {
 
-      # symlink plugins config
-      ln -s ${dokuwikiPluginsLocalConfig hostName cfg} $out/share/dokuwiki/plugins.local.php
+      page = mkOption {
+        type = types.str;
+        description = lib.mdDoc "Page or namespace to restrict";
+        example = "start";
+      };
 
-      # symlink acl
-      ln -s ${dokuwikiAclAuthConfig hostName cfg} $out/share/dokuwiki/acl.auth.php
+      actor = mkOption {
+        type = types.str;
+        description = lib.mdDoc "User or group to restrict";
+        example = "@external";
+      };
 
-      # symlink additional plugin(s) and templates(s)
-      ${concatMapStringsSep "\n" (template: "ln -s ${template} $out/share/dokuwiki/lib/tpl/${template.name}") cfg.templates}
-      ${concatMapStringsSep "\n" (plugin: "ln -s ${plugin} $out/share/dokuwiki/lib/plugins/${plugin.name}") cfg.plugins}
-    '';
+      level = let
+        available = {
+          "none" = 0;
+          "read" = 1;
+          "edit" = 2;
+          "create" = 4;
+          "upload" = 8;
+          "delete" = 16;
+        };
+      in mkOption {
+        type = types.enum ((attrValues available) ++ (attrNames available));
+        apply = x: if isInt x then x else available.${x};
+        description = lib.mdDoc ''
+          Permission level to restrict the actor(s) to.
+          See <https://www.dokuwiki.org/acl#background_info> for explanation
+        '';
+        example = "read";
+      };
+    };
   };
 
-  siteOpts = { config, lib, name, ... }:
+  # The current implementations of `doRename`,  `mkRenamedOptionModule` do not provide the full options path when used with submodules.
+  # They would only show `settings.useacl' instead of `services.dokuwiki.sites."site1.local".settings.useacl'
+  # The partial re-implementation of these functions is done to help users in debugging by showing the full path.
+  mkRenamed = from: to: { config, options, name, ... }: let
+    pathPrefix = [ "services" "dokuwiki" "sites" name ];
+    fromPath = pathPrefix  ++ from;
+    fromOpt = getAttrFromPath from options;
+    toOp = getAttrsFromPath to config;
+    toPath = pathPrefix ++ to;
+  in {
+    options = setAttrByPath from (mkOption {
+      visible = false;
+      description = lib.mdDoc "Alias of {option}${showOption toPath}";
+      apply = x: builtins.trace "Obsolete option `${showOption fromPath}' is used. It was renamed to ${showOption toPath}" toOp;
+    });
+    config = mkMerge [
+      {
+        warnings = optional fromOpt.isDefined
+          "The option `${showOption fromPath}' defined in ${showFiles fromOpt.files} has been renamed to `${showOption toPath}'.";
+      }
+      (lib.modules.mkAliasAndWrapDefsWithPriority (setAttrByPath to) fromOpt)
+    ];
+  };
+
+  siteOpts = { options, config, lib, name, ... }:
     {
+      imports = [
+        (mkRenamed [ "aclUse" ] [ "settings" "useacl" ])
+        (mkRenamed [ "superUser" ] [ "settings" "superuser" ])
+        (mkRenamed [ "disableActions" ] [ "settings"  "disableactions" ])
+        ({ config, options, ... }: let
+          showPath = suffix: lib.options.showOption ([ "services" "dokuwiki" "sites" name ] ++ suffix);
+          replaceExtraConfig = "Please use `${showPath ["settings"]}' to pass structured settings instead.";
+          ecOpt = options.extraConfig;
+          ecPath = showPath [ "extraConfig" ];
+        in {
+          options.extraConfig = mkOption {
+            visible = false;
+            apply = x: throw "The option ${ecPath} can no longer be used since it's been removed.\n${replaceExtraConfig}";
+          };
+          config.assertions = [
+            {
+              assertion = !ecOpt.isDefined;
+              message = "The option definition `${ecPath}' in ${showFiles ecOpt.files} no longer has any effect; please remove it.\n${replaceExtraConfig}";
+            }
+            {
+              assertion = config.mergedConfig.useacl -> (config.acl != null || config.aclFile != null);
+              message = "Either ${showPath [ "acl" ]} or ${showPath [ "aclFile" ]} is mandatory if ${showPath [ "settings" "useacl" ]} is true";
+            }
+            {
+              assertion = config.usersFile != null -> config.mergedConfig.useacl != false;
+              message = "${showPath [ "settings" "useacl" ]} is required when ${showPath [ "usersFile" ]} is set (Currently defined as `${config.usersFile}' in ${showFiles options.usersFile.files}).";
+            }
+          ];
+        })
+      ];
+
       options = {
-        enable = mkEnableOption (lib.mdDoc "DokuWiki web application.");
+        enable = mkEnableOption (lib.mdDoc "DokuWiki web application");
 
         package = mkOption {
           type = types.package;
@@ -75,9 +196,22 @@ let
         };
 
         acl = mkOption {
-          type = types.nullOr types.lines;
+          type = with types; nullOr (listOf (submodule aclOpts));
           default = null;
-          example = "*               @ALL               8";
+          example = literalExpression ''
+            [
+              {
+                page = "start";
+                actor = "@external";
+                level = "read";
+              }
+              {
+                page = "*";
+                actor = "@users";
+                level = "upload";
+              }
+            ]
+          '';
           description = lib.mdDoc ''
             Access Control Lists: see <https://www.dokuwiki.org/acl>
             Mutually exclusive with services.dokuwiki.aclFile
@@ -90,7 +224,7 @@ let
 
         aclFile = mkOption {
           type = with types; nullOr str;
-          default = if (config.aclUse && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null;
+          default = if (config.mergedConfig.useacl && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null;
           description = lib.mdDoc ''
             Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl
             Mutually exclusive with services.dokuwiki.acl which is preferred.
@@ -100,42 +234,22 @@ let
           example = "/var/lib/dokuwiki/${name}/acl.auth.php";
         };
 
-        aclUse = mkOption {
-          type = types.bool;
-          default = true;
-          description = lib.mdDoc ''
-            Necessary for users to log in into the system.
-            Also limits anonymous users. When disabled,
-            everyone is able to create and edit content.
-          '';
-        };
-
         pluginsConfig = mkOption {
-          type = types.lines;
-          default = ''
-            $plugins['authad'] = 0;
-            $plugins['authldap'] = 0;
-            $plugins['authmysql'] = 0;
-            $plugins['authpgsql'] = 0;
-          '';
+          type = with types; attrsOf bool;
+          default = {
+            authad = false;
+            authldap = false;
+            authmysql = false;
+            authpgsql = false;
+          };
           description = lib.mdDoc ''
             List of the dokuwiki (un)loaded plugins.
           '';
         };
 
-        superUser = mkOption {
-          type = types.nullOr types.str;
-          default = "@admin";
-          description = lib.mdDoc ''
-            You can set either a username, a list of usernames (“admin1,admin2”),
-            or the name of a group by prepending an @ char to the groupname
-            Consult documentation <https://www.dokuwiki.org/config:superuser> for further instructions.
-          '';
-        };
-
         usersFile = mkOption {
           type = with types; nullOr str;
-          default = if config.aclUse then "/var/lib/dokuwiki/${name}/users.auth.php" else null;
+          default = if config.mergedConfig.useacl then "/var/lib/dokuwiki/${name}/users.auth.php" else null;
           description = lib.mdDoc ''
             Location of the dokuwiki users file. List of users. Format:
 
@@ -150,17 +264,6 @@ let
           example = "/var/lib/dokuwiki/${name}/users.auth.php";
         };
 
-        disableActions = mkOption {
-          type = types.nullOr types.str;
-          default = "";
-          example = "search,register";
-          description = lib.mdDoc ''
-            Disable individual action modes. Refer to
-            <https://www.dokuwiki.org/config:action_modes>
-            for details on supported values.
-          '';
-        };
-
         plugins = mkOption {
           type = types.listOf types.path;
           default = [];
@@ -173,18 +276,14 @@ let
           '';
           example = literalExpression ''
                 let
-                  # Let's package the icalevents plugin
-                  plugin-icalevents = pkgs.stdenv.mkDerivation {
+                  plugin-icalevents = pkgs.stdenv.mkDerivation rec {
                     name = "icalevents";
-                    # Download the plugin from the dokuwiki site
-                    src = pkgs.fetchurl {
-                      url = "https://github.com/real-or-random/dokuwiki-plugin-icalevents/releases/download/2017-06-16/dokuwiki-plugin-icalevents-2017-06-16.zip";
-                      sha256 = "e40ed7dd6bbe7fe3363bbbecb4de481d5e42385b5a0f62f6a6ce6bf3a1f9dfa8";
+                    version = "2017-06-16";
+                    src = pkgs.fetchzip {
+                      stripRoot = false;
+                      url = "https://github.com/real-or-random/dokuwiki-plugin-icalevents/releases/download/''${version}/dokuwiki-plugin-icalevents-''${version}.zip";
+                      hash = "sha256-IPs4+qgEfe8AAWevbcCM9PnyI0uoyamtWeg4rEb+9Wc=";
                     };
-                    sourceRoot = ".";
-                    # We need unzip to build this package
-                    buildInputs = [ pkgs.unzip ];
-                    # Installing simply means copying all files to the output directory
                     installPhase = "mkdir -p $out; cp -R * $out/";
                   };
                 # And then pass this theme to the plugin list like this:
@@ -204,19 +303,17 @@ let
           '';
           example = literalExpression ''
                 let
-                  # Let's package the bootstrap3 theme
-                  template-bootstrap3 = pkgs.stdenv.mkDerivation {
-                    name = "bootstrap3";
-                    # Download the theme from the dokuwiki site
-                    src = pkgs.fetchurl {
-                      url = "https://github.com/giterlizzi/dokuwiki-template-bootstrap3/archive/v2019-05-22.zip";
-                      sha256 = "4de5ff31d54dd61bbccaf092c9e74c1af3a4c53e07aa59f60457a8f00cfb23a6";
-                    };
-                    # We need unzip to build this package
-                    buildInputs = [ pkgs.unzip ];
-                    # Installing simply means copying all files to the output directory
-                    installPhase = "mkdir -p $out; cp -R * $out/";
+                  template-bootstrap3 = pkgs.stdenv.mkDerivation rec {
+                  name = "bootstrap3";
+                  version = "2022-07-27";
+                  src = pkgs.fetchFromGitHub {
+                    owner = "giterlizzi";
+                    repo = "dokuwiki-template-bootstrap3";
+                    rev = "v''${version}";
+                    hash = "sha256-B3Yd4lxdwqfCnfmZdp+i/Mzwn/aEuZ0ovagDxuR6lxo=";
                   };
+                  installPhase = "mkdir -p $out; cp -R * $out/";
+                };
                 # And then pass this theme to the template list like this:
                 in [ template-bootstrap3 ]
           '';
@@ -238,26 +335,92 @@ let
           '';
         };
 
-        extraConfig = mkOption {
-          type = types.nullOr types.lines;
-          default = null;
-          example = ''
-            $conf['title'] = 'My Wiki';
-            $conf['userewrite'] = 1;
+        phpPackage = mkOption {
+          type = types.package;
+          relatedPackages = [ "php81" "php82" ];
+          default = pkgs.php81;
+          defaultText = "pkgs.php81";
+          description = lib.mdDoc ''
+            PHP package to use for this dokuwiki site.
           '';
+        };
+
+        phpOptions = mkOption {
+          type = types.attrsOf types.str;
+          default = {};
           description = lib.mdDoc ''
-            DokuWiki configuration. Refer to
-            <https://www.dokuwiki.org/config>
-            for details on supported values.
+            Options for PHP's php.ini file for this dokuwiki site.
+          '';
+          example = literalExpression ''
+          {
+            "opcache.interned_strings_buffer" = "8";
+            "opcache.max_accelerated_files" = "10000";
+            "opcache.memory_consumption" = "128";
+            "opcache.revalidate_freq" = "15";
+            "opcache.fast_shutdown" = "1";
+          }
           '';
         };
 
-      };
+        settings = mkOption {
+          type = types.attrsOf types.anything;
+          default = {
+            useacl = true;
+            superuser = "admin";
+          };
+          description = lib.mdDoc ''
+            Structural DokuWiki configuration.
+            Refer to <https://www.dokuwiki.org/config>
+            for details and supported values.
+            Settings can either be directly set from nix,
+            loaded from a file using `._file` or obtained from any
+            PHP function calls using `._raw`.
+          '';
+          example = literalExpression ''
+            {
+              title = "My Wiki";
+              userewrite = 1;
+              disableactions = [ "register" ]; # Will be concatenated with commas
+              plugin.smtp = {
+                smtp_pass._file = "/var/run/secrets/dokuwiki/smtp_pass";
+                smtp_user._raw = "getenv('DOKUWIKI_SMTP_USER')";
+              };
+            }
+          '';
+        };
 
+        mergedConfig = mkOption {
+          readOnly = true;
+          default = mergeConfig config;
+          defaultText = literalExpression ''
+            {
+              useacl = true;
+            }
+          '';
+          description = lib.mdDoc ''
+            Read only representation of the final configuration.
+          '';
+        };
+
+      # Required for the mkRenamedOptionModule
+      # TODO: Remove me once https://github.com/NixOS/nixpkgs/issues/96006 is fixed
+      # or we don't have any more notes about the removal of extraConfig, ...
+      warnings = mkOption {
+        type = types.listOf types.unspecified;
+        default = [ ];
+        visible = false;
+        internal = true;
+      };
+      assertions = mkOption {
+        type = types.listOf types.unspecified;
+        default = [ ];
+        visible = false;
+        internal = true;
+      };
     };
+  };
 in
 {
-  # interface
   options = {
     services.dokuwiki = {
 
@@ -276,8 +439,8 @@ in
           Further nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`.
           See [](#opt-services.nginx.virtualHosts) for further information.
 
-          Further apache2 configuration can be done by adapting `services.httpd.virtualHosts.<name>`.
-          See [](#opt-services.httpd.virtualHosts) for further information.
+          Further caddy configuration can be done by adapting `services.caddy.virtualHosts.<name>`.
+          See [](#opt-services.caddy.virtualHosts) for further information.
         '';
       };
 
@@ -287,29 +450,19 @@ in
   # implementation
   config = mkIf (eachSite != {}) (mkMerge [{
 
-    assertions = flatten (mapAttrsToList (hostName: cfg:
-    [{
-      assertion = cfg.aclUse -> (cfg.acl != null || cfg.aclFile != null);
-      message = "Either services.dokuwiki.sites.${hostName}.acl or services.dokuwiki.sites.${hostName}.aclFile is mandatory if aclUse true";
-    }
-    {
-      assertion = cfg.usersFile != null -> cfg.aclUse != false;
-      message = "services.dokuwiki.sites.${hostName}.aclUse must must be true if usersFile is not null";
-    }
-    ]) eachSite);
+    warnings = flatten (mapAttrsToList (_: cfg: cfg.warnings) eachSite);
+
+    assertions = flatten (mapAttrsToList (_: cfg: cfg.assertions) eachSite);
 
     services.phpfpm.pools = mapAttrs' (hostName: cfg: (
       nameValuePair "dokuwiki-${hostName}" {
         inherit user;
         group = webserver.group;
 
-        phpPackage = pkgs.php81;
-        phpEnv = {
-          DOKUWIKI_LOCAL_CONFIG = "${dokuwikiLocalConfig hostName cfg}";
-          DOKUWIKI_PLUGINS_LOCAL_CONFIG = "${dokuwikiPluginsLocalConfig hostName cfg}";
-        } // optionalAttrs (cfg.usersFile != null) {
+        phpPackage = mkPhpPackage cfg;
+        phpEnv = optionalAttrs (cfg.usersFile != null) {
           DOKUWIKI_USERS_AUTH_CONFIG = "${cfg.usersFile}";
-        } //optionalAttrs (cfg.aclUse) {
+        } // optionalAttrs (cfg.mergedConfig.useacl) {
           DOKUWIKI_ACL_AUTH_CONFIG = if (cfg.acl != null) then "${dokuwikiAclAuthConfig hostName cfg}" else "${toString cfg.aclFile}";
         };
 
@@ -444,5 +597,6 @@ in
     _1000101
     onny
     dandellion
+    e1mo
   ];
 }
diff --git a/nixos/modules/services/web-apps/dolibarr.nix b/nixos/modules/services/web-apps/dolibarr.nix
index f262099354d..453229c130c 100644
--- a/nixos/modules/services/web-apps/dolibarr.nix
+++ b/nixos/modules/services/web-apps/dolibarr.nix
@@ -5,7 +5,7 @@ let
   package = pkgs.dolibarr.override { inherit (cfg) stateDir; };
 
   cfg = config.services.dolibarr;
-  vhostCfg = lib.optionalAttr (cfg.nginx != null) config.services.nginx.virtualHosts."${cfg.domain}";
+  vhostCfg = lib.optionalAttrs (cfg.nginx != null) config.services.nginx.virtualHosts."${cfg.domain}";
 
   mkConfigFile = filename: settings:
     let
@@ -16,7 +16,7 @@ let
         if (any (str: k == str) secretKeys) then v
         else if isString v then "'${v}'"
         else if isBool v then boolToString v
-        else if isNull v then "null"
+        else if v == null then "null"
         else toString v
       ;
     in
diff --git a/nixos/modules/services/web-apps/freshrss.nix b/nixos/modules/services/web-apps/freshrss.nix
index c05e7b2c4f7..89e29f7ccb5 100644
--- a/nixos/modules/services/web-apps/freshrss.nix
+++ b/nixos/modules/services/web-apps/freshrss.nix
@@ -60,7 +60,7 @@ in
       };
 
       port = mkOption {
-        type = with types; nullOr port;
+        type = types.nullOr types.port;
         default = null;
         description = mdDoc "Database port for FreshRSS.";
         example = 3306;
@@ -73,7 +73,7 @@ in
       };
 
       passFile = mkOption {
-        type = types.nullOr types.str;
+        type = types.nullOr types.path;
         default = null;
         description = mdDoc "Database password file for FreshRSS.";
         example = "/run/secrets/freshrss";
@@ -116,12 +116,18 @@ in
         with default values.
       '';
     };
-  };
 
+    user = mkOption {
+      type = types.str;
+      default = "freshrss";
+      description = lib.mdDoc "User under which Freshrss runs.";
+    };
+  };
 
   config =
     let
-      systemd-hardening = {
+      defaultServiceConfig = {
+        ReadWritePaths = "${cfg.dataDir}";
         CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
         DeviceAllow = "";
         LockPersonality = true;
@@ -146,6 +152,11 @@ in
         SystemCallArchitectures = "native";
         SystemCallFilter = [ "@system-service" "~@resources" "~@privileged" ];
         UMask = "0007";
+        Type = "oneshot";
+        User = cfg.user;
+        Group = config.users.users.${cfg.user}.group;
+        StateDirectory = "freshrss";
+        WorkingDirectory = cfg.package;
       };
     in
     mkIf cfg.enable {
@@ -199,12 +210,17 @@ in
         };
       };
 
-      users.users.freshrss = {
+      users.users."${cfg.user}" = {
         description = "FreshRSS service user";
         isSystemUser = true;
-        group = "freshrss";
+        group = "${cfg.user}";
+        home = cfg.dataDir;
       };
-      users.groups.freshrss = { };
+      users.groups."${cfg.user}" = { };
+
+      systemd.tmpfiles.rules = [
+        "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
+      ];
 
       systemd.services.freshrss-config =
         let
@@ -228,30 +244,24 @@ in
         {
           description = "Set up the state directory for FreshRSS before use";
           wantedBy = [ "multi-user.target" ];
-          serviceConfig = {
+          serviceConfig = defaultServiceConfig //{
             Type = "oneshot";
             User = "freshrss";
             Group = "freshrss";
             StateDirectory = "freshrss";
             WorkingDirectory = cfg.package;
-          } // systemd-hardening;
+          };
           environment = {
             FRESHRSS_DATA_PATH = cfg.dataDir;
           };
 
           script = ''
-            # create files with correct permissions
-            mkdir -m 755 -p ${cfg.dataDir}
-
             # do installation or reconfigure
             if test -f ${cfg.dataDir}/config.php; then
               # reconfigure with settings
               ./cli/reconfigure.php ${settingsFlags}
               ./cli/update-user.php --user ${cfg.defaultUser} --password "$(cat ${cfg.passwordFile})"
             else
-              # Copy the user data template directory
-              cp -r ./data ${cfg.dataDir}
-
               # check correct folders in data folder
               ./cli/prepare.php
               # install with settings
@@ -269,14 +279,9 @@ in
         environment = {
           FRESHRSS_DATA_PATH = cfg.dataDir;
         };
-        serviceConfig = {
-          Type = "oneshot";
-          User = "freshrss";
-          Group = "freshrss";
-          StateDirectory = "freshrss";
-          WorkingDirectory = cfg.package;
+        serviceConfig = defaultServiceConfig //{
           ExecStart = "${cfg.package}/app/actualize_script.php";
-        } // systemd-hardening;
+        };
       };
     };
 }
diff --git a/nixos/modules/services/web-apps/galene.nix b/nixos/modules/services/web-apps/galene.nix
index 15ef09aa0b8..747b85f94c6 100644
--- a/nixos/modules/services/web-apps/galene.nix
+++ b/nixos/modules/services/web-apps/galene.nix
@@ -12,7 +12,7 @@ in
 {
   options = {
     services.galene = {
-      enable = mkEnableOption (lib.mdDoc "Galene Service.");
+      enable = mkEnableOption (lib.mdDoc "Galene Service");
 
       stateDir = mkOption {
         default = defaultstateDir;
diff --git a/nixos/modules/services/web-apps/gotosocial.md b/nixos/modules/services/web-apps/gotosocial.md
new file mode 100644
index 00000000000..a290d7d1893
--- /dev/null
+++ b/nixos/modules/services/web-apps/gotosocial.md
@@ -0,0 +1,64 @@
+# GoToSocial {#module-services-gotosocial}
+
+[GoToSocial](https://gotosocial.org/) is an ActivityPub social network server, written in Golang.
+
+## Service configuration {#modules-services-gotosocial-service-configuration}
+
+The following configuration sets up the PostgreSQL as database backend and binds
+GoToSocial to `127.0.0.1:8080`, expecting to be run behind a HTTP proxy on `gotosocial.example.com`.
+
+```nix
+services.gotosocial = {
+  enable = true;
+  setupPostgresqlDB = true;
+  settings = {
+    application-name = "My GoToSocial";
+    host = "gotosocial.example.com";
+    protocol = "https";
+    bind-address = "127.0.0.1";
+    port = 8080;
+  };
+};
+```
+
+Please refer to the [GoToSocial Documentation](https://docs.gotosocial.org/en/latest/configuration/general/)
+for additional configuration options.
+
+## Proxy configuration {#modules-services-gotosocial-proxy-configuration}
+
+Although it is possible to expose GoToSocial directly, it is common practice to operate it behind an
+HTTP reverse proxy such as nginx.
+
+```nix
+networking.firewall.allowedTCPPorts = [ 80 443 ];
+services.nginx = {
+  enable = true;
+  clientMaxBodySize = "40M";
+  virtualHosts = with config.services.gotosocial.settings; {
+    "${host}" = {
+      enableACME = true;
+      forceSSL = true;
+      locations = {
+        "/" = {
+          recommendedProxySettings = true;
+          proxyWebsockets = true;
+          proxyPass = "http://${bind-address}:${toString port}";
+        };
+      };
+    };
+  };
+};
+```
+
+Please refer to [](#module-security-acme) for details on how to provision an SSL/TLS certificate.
+
+## User management {#modules-services-gotosocial-user-management}
+
+After the GoToSocial service is running, the `gotosocial-admin` utility can be used to manage users. In particular an
+administrative user can be created with
+
+```ShellSession
+$ sudo gotosocial-admin account create --username <nickname> --email <email> --password <password>
+$ sudo gotosocial-admin account confirm --username <nickname>
+$ sudo gotosocial-admin account promote --username <nickname>
+```
diff --git a/nixos/modules/services/web-apps/gotosocial.nix b/nixos/modules/services/web-apps/gotosocial.nix
new file mode 100644
index 00000000000..f7ae018d5b7
--- /dev/null
+++ b/nixos/modules/services/web-apps/gotosocial.nix
@@ -0,0 +1,173 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.gotosocial;
+  settingsFormat = pkgs.formats.yaml { };
+  configFile = settingsFormat.generate "config.yml" cfg.settings;
+  defaultSettings = {
+    application-name = "gotosocial";
+
+    protocol = "https";
+
+    bind-address = "127.0.0.1";
+    port = 8080;
+
+    storage-local-base-path = "/var/lib/gotosocial/storage";
+
+    db-type = "sqlite";
+    db-address = "/var/lib/gotosocial/database.sqlite";
+  };
+  gotosocial-admin = pkgs.writeShellScriptBin "gotosocial-admin" ''
+    exec systemd-run \
+      -u gotosocial-admin.service \
+      -p Group=gotosocial \
+      -p User=gotosocial \
+      -q -t -G --wait --service-type=exec \
+      ${cfg.package}/bin/gotosocial --config-path ${configFile} admin "$@"
+  '';
+in
+{
+  meta.doc = ./gotosocial.md;
+  meta.maintainers = with lib.maintainers; [ misuzu ];
+
+  options.services.gotosocial = {
+    enable = lib.mkEnableOption (lib.mdDoc "ActivityPub social network server");
+
+    package = lib.mkPackageOptionMD pkgs "gotosocial" { };
+
+    openFirewall = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Open the configured port in the firewall.
+        Using a reverse proxy instead is highly recommended.
+      '';
+    };
+
+    setupPostgresqlDB = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Whether to setup a local postgres database and populate the
+        `db-type` fields in `services.gotosocial.settings`.
+      '';
+    };
+
+    settings = lib.mkOption {
+      type = settingsFormat.type;
+      default = defaultSettings;
+      example = {
+        application-name = "My GoToSocial";
+        host = "gotosocial.example.com";
+      };
+      description = lib.mdDoc ''
+        Contents of the GoToSocial YAML config.
+
+        Please refer to the
+        [documentation](https://docs.gotosocial.org/en/latest/configuration/)
+        and
+        [example config](https://github.com/superseriousbusiness/gotosocial/blob/main/example/config.yaml).
+
+        Please note that the `host` option cannot be changed later so it is important to configure this correctly before you start GoToSocial.
+      '';
+    };
+
+    environmentFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      description = lib.mdDoc ''
+        File path containing environment variables for configuring the GoToSocial service
+        in the format of an EnvironmentFile as described by systemd.exec(5).
+
+        This option could be used to pass sensitive configuration to the GoToSocial daemon.
+
+        Please refer to the Environment Variables section in the
+        [documentation](https://docs.gotosocial.org/en/latest/configuration/).
+      '';
+      default = null;
+      example = "/root/nixos/secrets/gotosocial.env";
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.settings.host or null != null;
+        message = ''
+          You have to define a hostname for GoToSocial (`services.gotosocial.settings.host`), it cannot be changed later without starting over!
+        '';
+      }
+    ];
+
+    services.gotosocial.settings = (lib.mapAttrs (name: lib.mkDefault) (
+      defaultSettings // {
+        web-asset-base-dir = "${cfg.package}/share/gotosocial/web/assets/";
+        web-template-base-dir = "${cfg.package}/share/gotosocial/web/template/";
+      }
+    )) // (lib.optionalAttrs cfg.setupPostgresqlDB {
+      db-type = "postgres";
+      db-address = "/run/postgresql";
+      db-database = "gotosocial";
+      db-user = "gotosocial";
+    });
+
+    environment.systemPackages = [ gotosocial-admin ];
+
+    users.groups.gotosocial = { };
+    users.users.gotosocial = {
+      group = "gotosocial";
+      isSystemUser = true;
+    };
+
+    networking.firewall = lib.mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.settings.port ];
+    };
+
+    services.postgresql = lib.mkIf cfg.setupPostgresqlDB {
+      enable = true;
+      ensureDatabases = [ "gotosocial" ];
+      ensureUsers = [
+        {
+          name = "gotosocial";
+          ensurePermissions = {
+            "DATABASE gotosocial" = "ALL PRIVILEGES";
+          };
+        }
+      ];
+    };
+
+    systemd.services.gotosocial = {
+      description = "ActivityPub social network server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ]
+        ++ lib.optional cfg.setupPostgresqlDB "postgresql.service";
+      requires = lib.optional cfg.setupPostgresqlDB "postgresql.service";
+      restartTriggers = [ configFile ];
+
+      serviceConfig = {
+        EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
+        ExecStart = "${cfg.package}/bin/gotosocial --config-path ${configFile} server start";
+        Restart = "on-failure";
+        Group = "gotosocial";
+        User = "gotosocial";
+        StateDirectory = "gotosocial";
+        WorkingDirectory = "/var/lib/gotosocial";
+
+        # Security options:
+        # Based on https://github.com/superseriousbusiness/gotosocial/blob/v0.8.1/example/gotosocial.service
+        AmbientCapabilities = lib.optional (cfg.settings.port < 1024) "CAP_NET_BIND_SERVICE";
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        DevicePolicy = "closed";
+        ProtectSystem = "full";
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        LockPersonality = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/grocy.md b/nixos/modules/services/web-apps/grocy.md
new file mode 100644
index 00000000000..62aad4b103d
--- /dev/null
+++ b/nixos/modules/services/web-apps/grocy.md
@@ -0,0 +1,66 @@
+# Grocy {#module-services-grocy}
+
+[Grocy](https://grocy.info/) is a web-based self-hosted groceries
+& household management solution for your home.
+
+## Basic usage {#module-services-grocy-basic-usage}
+
+A very basic configuration may look like this:
+```
+{ pkgs, ... }:
+{
+  services.grocy = {
+    enable = true;
+    hostName = "grocy.tld";
+  };
+}
+```
+This configures a simple vhost using [nginx](#opt-services.nginx.enable)
+which listens to `grocy.tld` with fully configured ACME/LE (this can be
+disabled by setting [services.grocy.nginx.enableSSL](#opt-services.grocy.nginx.enableSSL)
+to `false`). After the initial setup the credentials `admin:admin`
+can be used to login.
+
+The application's state is persisted at `/var/lib/grocy/grocy.db` in a
+`sqlite3` database. The migration is applied when requesting the `/`-route
+of the application.
+
+## Settings {#module-services-grocy-settings}
+
+The configuration for `grocy` is located at `/etc/grocy/config.php`.
+By default, the following settings can be defined in the NixOS-configuration:
+```
+{ pkgs, ... }:
+{
+  services.grocy.settings = {
+    # The default currency in the system for invoices etc.
+    # Please note that exchange rates aren't taken into account, this
+    # is just the setting for what's shown in the frontend.
+    currency = "EUR";
+
+    # The display language (and locale configuration) for grocy.
+    culture = "de";
+
+    calendar = {
+      # Whether or not to show the week-numbers
+      # in the calendar.
+      showWeekNumber = true;
+
+      # Index of the first day to be shown in the calendar (0=Sunday, 1=Monday,
+      # 2=Tuesday and so on).
+      firstDayOfWeek = 2;
+    };
+  };
+}
+```
+
+If you want to alter the configuration file on your own, you can do this manually with
+an expression like this:
+```
+{ lib, ... }:
+{
+  environment.etc."grocy/config.php".text = lib.mkAfter ''
+    // Arbitrary PHP code in grocy's configuration file
+  '';
+}
+```
diff --git a/nixos/modules/services/web-apps/grocy.nix b/nixos/modules/services/web-apps/grocy.nix
index 6efc2ccfd30..688367cafaf 100644
--- a/nixos/modules/services/web-apps/grocy.nix
+++ b/nixos/modules/services/web-apps/grocy.nix
@@ -117,7 +117,9 @@ in {
 
       # PHP 8.0 is the only version which is supported/tested by upstream:
       # https://github.com/grocy/grocy/blob/v3.3.0/README.md#how-to-install
-      phpPackage = pkgs.php80;
+      # Compatibility with PHP 8.1 is available on their development branch:
+      # https://github.com/grocy/grocy/commit/38a4ad8ec480c29a1bff057b3482fd103b036848
+      phpPackage = pkgs.php81;
 
       inherit (cfg.phpfpm) settings;
 
@@ -167,6 +169,6 @@ in {
 
   meta = {
     maintainers = with maintainers; [ ma27 ];
-    doc = ./grocy.xml;
+    doc = ./grocy.md;
   };
 }
diff --git a/nixos/modules/services/web-apps/grocy.xml b/nixos/modules/services/web-apps/grocy.xml
deleted file mode 100644
index fdf6d00f4b1..00000000000
--- a/nixos/modules/services/web-apps/grocy.xml
+++ /dev/null
@@ -1,77 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-grocy">
-
-  <title>Grocy</title>
-  <para>
-    <link xlink:href="https://grocy.info/">Grocy</link> is a web-based self-hosted groceries
-    &amp; household management solution for your home.
-  </para>
-
-  <section xml:id="module-services-grocy-basic-usage">
-   <title>Basic usage</title>
-   <para>
-    A very basic configuration may look like this:
-<programlisting>{ pkgs, ... }:
-{
-  services.grocy = {
-    <link linkend="opt-services.grocy.enable">enable</link> = true;
-    <link linkend="opt-services.grocy.hostName">hostName</link> = "grocy.tld";
-  };
-}</programlisting>
-    This configures a simple vhost using <link linkend="opt-services.nginx.enable">nginx</link>
-    which listens to <literal>grocy.tld</literal> with fully configured ACME/LE (this can be
-    disabled by setting <link linkend="opt-services.grocy.nginx.enableSSL">services.grocy.nginx.enableSSL</link>
-    to <literal>false</literal>). After the initial setup the credentials <literal>admin:admin</literal>
-    can be used to login.
-   </para>
-   <para>
-    The application's state is persisted at <literal>/var/lib/grocy/grocy.db</literal> in a
-    <package>sqlite3</package> database. The migration is applied when requesting the <literal>/</literal>-route
-    of the application.
-   </para>
-  </section>
-
-  <section xml:id="module-services-grocy-settings">
-   <title>Settings</title>
-   <para>
-    The configuration for <literal>grocy</literal> is located at <literal>/etc/grocy/config.php</literal>.
-    By default, the following settings can be defined in the NixOS-configuration:
-<programlisting>{ pkgs, ... }:
-{
-  services.grocy.settings = {
-    # The default currency in the system for invoices etc.
-    # Please note that exchange rates aren't taken into account, this
-    # is just the setting for what's shown in the frontend.
-    <link linkend="opt-services.grocy.settings.currency">currency</link> = "EUR";
-
-    # The display language (and locale configuration) for grocy.
-    <link linkend="opt-services.grocy.settings.currency">culture</link> = "de";
-
-    calendar = {
-      # Whether or not to show the week-numbers
-      # in the calendar.
-      <link linkend="opt-services.grocy.settings.calendar.showWeekNumber">showWeekNumber</link> = true;
-
-      # Index of the first day to be shown in the calendar (0=Sunday, 1=Monday,
-      # 2=Tuesday and so on).
-      <link linkend="opt-services.grocy.settings.calendar.firstDayOfWeek">firstDayOfWeek</link> = 2;
-    };
-  };
-}</programlisting>
-   </para>
-   <para>
-    If you want to alter the configuration file on your own, you can do this manually with
-    an expression like this:
-<programlisting>{ lib, ... }:
-{
-  environment.etc."grocy/config.php".text = lib.mkAfter ''
-    // Arbitrary PHP code in grocy's configuration file
-  '';
-}</programlisting>
-   </para>
-  </section>
-
-</chapter>
diff --git a/nixos/modules/services/web-apps/guacamole-client.nix b/nixos/modules/services/web-apps/guacamole-client.nix
new file mode 100644
index 00000000000..c12f6582468
--- /dev/null
+++ b/nixos/modules/services/web-apps/guacamole-client.nix
@@ -0,0 +1,60 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+let
+  cfg = config.services.guacamole-client;
+  settingsFormat = pkgs.formats.javaProperties { };
+in
+{
+  options = {
+    services.guacamole-client = {
+      enable = lib.mkEnableOption (lib.mdDoc "Apache Guacamole Client (Tomcat)");
+      package = lib.mkPackageOptionMD pkgs "guacamole-client" { };
+
+      settings = lib.mkOption {
+        type = lib.types.submodule {
+          freeformType = settingsFormat.type;
+        };
+        default = {
+          guacd-hostname = "localhost";
+          guacd-port = 4822;
+        };
+        description = lib.mdDoc ''
+          Configuration written to `guacamole.properties`.
+
+          ::: {.note}
+          The Guacamole web application uses one main configuration file called
+          `guacamole.properties`. This file is the common location for all
+          configuration properties read by Guacamole or any extension of
+          Guacamole, including authentication providers.
+          :::
+        '';
+      };
+
+      enableWebserver = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = lib.mdDoc ''
+          Enable the Guacamole web application in a Tomcat webserver.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.etc."guacamole/guacamole.properties" = lib.mkIf
+      (cfg.settings != {})
+      { source = (settingsFormat.generate "guacamole.properties" cfg.settings); };
+
+    services = lib.mkIf cfg.enableWebserver {
+      tomcat = {
+        enable = true;
+        webapps = [
+          cfg.package
+        ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/guacamole-server.nix b/nixos/modules/services/web-apps/guacamole-server.nix
new file mode 100644
index 00000000000..0cffdce83d8
--- /dev/null
+++ b/nixos/modules/services/web-apps/guacamole-server.nix
@@ -0,0 +1,83 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+let
+  cfg = config.services.guacamole-server;
+in
+{
+  options = {
+    services.guacamole-server = {
+      enable = lib.mkEnableOption (lib.mdDoc "Apache Guacamole Server (guacd)");
+      package = lib.mkPackageOptionMD pkgs "guacamole-server" { };
+
+      extraEnvironment = lib.mkOption {
+        type = lib.types.attrsOf lib.types.str;
+        default = { };
+        example = lib.literalExpression ''
+          {
+            ENVIRONMENT = "production";
+          }
+        '';
+        description = lib.mdDoc "Environment variables to pass to guacd.";
+      };
+
+      host = lib.mkOption {
+        default = "127.0.0.1";
+        description = lib.mdDoc ''
+          The host name or IP address the server should listen to.
+        '';
+        type = lib.types.str;
+      };
+
+      port = lib.mkOption {
+        default = 4822;
+        description = lib.mdDoc ''
+          The port the guacd server should listen to.
+        '';
+        type = lib.types.port;
+      };
+
+      logbackXml = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        example = "/path/to/logback.xml";
+        description = lib.mdDoc ''
+          Configuration file that correspond to `logback.xml`.
+        '';
+      };
+
+      userMappingXml = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        example = "/path/to/user-mapping.xml";
+        description = lib.mdDoc ''
+          Configuration file that correspond to `user-mapping.xml`.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    # Setup configuration files.
+    environment.etc."guacamole/logback.xml" = lib.mkIf (cfg.logbackXml != null) { source = cfg.logbackXml; };
+    environment.etc."guacamole/user-mapping.xml" = lib.mkIf (cfg.userMappingXml != null) { source = cfg.userMappingXml; };
+
+    systemd.services.guacamole-server = {
+      description = "Apache Guacamole server (guacd)";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      environment = {
+        HOME = "/run/guacamole-server";
+      } // cfg.extraEnvironment;
+      serviceConfig = {
+        ExecStart = "${lib.getExe cfg.package} -f -b ${cfg.host} -l ${toString cfg.port}";
+        RuntimeDirectory = "guacamole-server";
+        DynamicUser = true;
+        PrivateTmp = "yes";
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/hedgedoc.nix b/nixos/modules/services/web-apps/hedgedoc.nix
index 90ca3002c59..e2014a9b7e3 100644
--- a/nixos/modules/services/web-apps/hedgedoc.nix
+++ b/nixos/modules/services/web-apps/hedgedoc.nix
@@ -624,7 +624,8 @@ in
               '';
             };
             clientSecret = mkOption {
-              type = types.str;
+              type = with types; nullOr str;
+              default = null;
               description = lib.mdDoc ''
                 Specify the OAuth client secret.
               '';
@@ -950,16 +951,16 @@ in
                 type = types.str;
                 default = "";
                 description = lib.mdDoc ''
-                  Attribute map for `id'.
-                  Defaults to `NameID' of SAML response.
+                  Attribute map for `id`.
+                  Defaults to `NameID` of SAML response.
                 '';
               };
               username = mkOption {
                 type = types.str;
                 default = "";
                 description = lib.mdDoc ''
-                  Attribute map for `username'.
-                  Defaults to `NameID' of SAML response.
+                  Attribute map for `username`.
+                  Defaults to `NameID` of SAML response.
                 '';
               };
               email = mkOption {
diff --git a/nixos/modules/services/web-apps/hledger-web.nix b/nixos/modules/services/web-apps/hledger-web.nix
index 86716a02649..0fc283ff521 100644
--- a/nixos/modules/services/web-apps/hledger-web.nix
+++ b/nixos/modules/services/web-apps/hledger-web.nix
@@ -7,7 +7,7 @@ in {
 
     enable = mkEnableOption (lib.mdDoc "hledger-web service");
 
-    serveApi = mkEnableOption (lib.mdDoc "Serve only the JSON web API, without the web UI.");
+    serveApi = mkEnableOption (lib.mdDoc "Serve only the JSON web API, without the web UI");
 
     host = mkOption {
       type = types.str;
diff --git a/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix
deleted file mode 100644
index a61aa445f82..00000000000
--- a/nixos/modules/services/web-apps/ihatemoney/default.nix
+++ /dev/null
@@ -1,153 +0,0 @@
-{ config, pkgs, lib, ... }:
-with lib;
-let
-  cfg = config.services.ihatemoney;
-  user = "ihatemoney";
-  group = "ihatemoney";
-  db = "ihatemoney";
-  python3 = config.services.uwsgi.package.python3;
-  pkg = python3.pkgs.ihatemoney;
-  toBool = x: if x then "True" else "False";
-  configFile = pkgs.writeText "ihatemoney.cfg" ''
-        from secrets import token_hex
-        # load a persistent secret key
-        SECRET_KEY_FILE = "/var/lib/ihatemoney/secret_key"
-        SECRET_KEY = ""
-        try:
-          with open(SECRET_KEY_FILE) as f:
-            SECRET_KEY = f.read()
-        except FileNotFoundError:
-          pass
-        if not SECRET_KEY:
-          print("ihatemoney: generating a new secret key")
-          SECRET_KEY = token_hex(50)
-          with open(SECRET_KEY_FILE, "w") as f:
-            f.write(SECRET_KEY)
-        del token_hex
-        del SECRET_KEY_FILE
-
-        # "normal" configuration
-        DEBUG = False
-        SQLALCHEMY_DATABASE_URI = '${
-          if cfg.backend == "sqlite"
-          then "sqlite:////var/lib/ihatemoney/ihatemoney.sqlite"
-          else "postgresql:///${db}"}'
-        SQLALCHEMY_TRACK_MODIFICATIONS = False
-        MAIL_DEFAULT_SENDER = (r"${cfg.defaultSender.name}", r"${cfg.defaultSender.email}")
-        ACTIVATE_DEMO_PROJECT = ${toBool cfg.enableDemoProject}
-        ADMIN_PASSWORD = r"${toString cfg.adminHashedPassword /*toString null == ""*/}"
-        ALLOW_PUBLIC_PROJECT_CREATION = ${toBool cfg.enablePublicProjectCreation}
-        ACTIVATE_ADMIN_DASHBOARD = ${toBool cfg.enableAdminDashboard}
-        SESSION_COOKIE_SECURE = ${toBool cfg.secureCookie}
-        ENABLE_CAPTCHA = ${toBool cfg.enableCaptcha}
-        LEGAL_LINK = r"${toString cfg.legalLink}"
-
-        ${cfg.extraConfig}
-  '';
-in
-  {
-    options.services.ihatemoney = {
-      enable = mkEnableOption (lib.mdDoc "ihatemoney webapp. Note that this will set uwsgi to emperor mode");
-      backend = mkOption {
-        type = types.enum [ "sqlite" "postgresql" ];
-        default = "sqlite";
-        description = lib.mdDoc ''
-          The database engine to use for ihatemoney.
-          If `postgresql` is selected, then a database called
-          `${db}` will be created. If you disable this option,
-          it will however not be removed.
-        '';
-      };
-      adminHashedPassword = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = lib.mdDoc "The hashed password of the administrator. To obtain it, run `ihatemoney generate_password_hash`";
-      };
-      uwsgiConfig = mkOption {
-        type = types.attrs;
-        example = {
-          http = ":8000";
-        };
-        description = lib.mdDoc "Additional configuration of the UWSGI vassal running ihatemoney. It should notably specify on which interfaces and ports the vassal should listen.";
-      };
-      defaultSender = {
-        name = mkOption {
-          type = types.str;
-          default = "Budget manager";
-          description = lib.mdDoc "The display name of the sender of ihatemoney emails";
-        };
-        email = mkOption {
-          type = types.str;
-          default = "ihatemoney@${config.networking.hostName}";
-          defaultText = literalExpression ''"ihatemoney@''${config.networking.hostName}"'';
-          description = lib.mdDoc "The email of the sender of ihatemoney emails";
-        };
-      };
-      secureCookie = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc "Use secure cookies. Disable this when ihatemoney is served via http instead of https";
-      };
-      enableDemoProject = mkEnableOption (lib.mdDoc "access to the demo project in ihatemoney");
-      enablePublicProjectCreation = mkEnableOption (lib.mdDoc "permission to create projects in ihatemoney by anyone");
-      enableAdminDashboard = mkEnableOption (lib.mdDoc "ihatemoney admin dashboard");
-      enableCaptcha = mkEnableOption (lib.mdDoc "a simplistic captcha for some forms");
-      legalLink = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = lib.mdDoc "The URL to a page explaining legal statements about your service, eg. GDPR-related information.";
-      };
-      extraConfig = mkOption {
-        type = types.str;
-        default = "";
-        description = lib.mdDoc "Extra configuration appended to ihatemoney's configuration file. It is a python file, so pay attention to indentation.";
-      };
-    };
-    config = mkIf cfg.enable {
-      services.postgresql = mkIf (cfg.backend == "postgresql") {
-        enable = true;
-        ensureDatabases = [ db ];
-        ensureUsers = [ {
-          name = user;
-          ensurePermissions = {
-            "DATABASE ${db}" = "ALL PRIVILEGES";
-          };
-        } ];
-      };
-      systemd.services.postgresql = mkIf (cfg.backend == "postgresql") {
-        wantedBy = [ "uwsgi.service" ];
-        before = [ "uwsgi.service" ];
-      };
-      systemd.tmpfiles.rules = [
-        "d /var/lib/ihatemoney 770 ${user} ${group}"
-      ];
-      users = {
-        users.${user} = {
-          isSystemUser = true;
-          inherit group;
-        };
-        groups.${group} = {};
-      };
-      services.uwsgi = {
-        enable = true;
-        plugins = [ "python3" ];
-        instance = {
-          type = "emperor";
-          vassals.ihatemoney = {
-            type = "normal";
-            strict = true;
-            immediate-uid = user;
-            immediate-gid = group;
-            # apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c
-            enable-threads = true;
-            module = "wsgi:application";
-            chdir = "${pkg}/${pkg.pythonModule.sitePackages}/ihatemoney";
-            env = [ "IHATEMONEY_SETTINGS_FILE_PATH=${configFile}" ];
-            pythonPackages = self: [ self.ihatemoney ];
-          } // cfg.uwsgiConfig;
-        };
-      };
-    };
-  }
-
-
diff --git a/nixos/modules/services/web-apps/invidious.nix b/nixos/modules/services/web-apps/invidious.nix
index 61c52ee03dc..e875be47fb9 100644
--- a/nixos/modules/services/web-apps/invidious.nix
+++ b/nixos/modules/services/web-apps/invidious.nix
@@ -41,6 +41,12 @@ let
         RestrictNamespaces = true;
         SystemCallArchitectures = "native";
         SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+
+        # Because of various issues Invidious must be restarted often, at least once a day, ideally
+        # every hour.
+        # This option enables the automatic restarting of the Invidious instance.
+        Restart = lib.mkDefault "always";
+        RuntimeMaxSec = lib.mkDefault "1h";
       };
     };
 
diff --git a/nixos/modules/services/web-apps/jirafeau.nix b/nixos/modules/services/web-apps/jirafeau.nix
index 293cbb3af42..b2e27416716 100644
--- a/nixos/modules/services/web-apps/jirafeau.nix
+++ b/nixos/modules/services/web-apps/jirafeau.nix
@@ -36,7 +36,7 @@ in
       description = lib.mdDoc "Location of Jirafeau storage directory.";
     };
 
-    enable = mkEnableOption (lib.mdDoc "Jirafeau file upload application.");
+    enable = mkEnableOption (lib.mdDoc "Jirafeau file upload application");
 
     extraConfig = mkOption {
       type = types.lines;
diff --git a/nixos/modules/services/web-apps/jitsi-meet.md b/nixos/modules/services/web-apps/jitsi-meet.md
new file mode 100644
index 00000000000..060ef975265
--- /dev/null
+++ b/nixos/modules/services/web-apps/jitsi-meet.md
@@ -0,0 +1,45 @@
+# Jitsi Meet {#module-services-jitsi-meet}
+
+With Jitsi Meet on NixOS you can quickly configure a complete,
+private, self-hosted video conferencing solution.
+
+## Basic usage {#module-services-jitsi-basic-usage}
+
+A minimal configuration using Let's Encrypt for TLS certificates looks like this:
+```
+{
+  services.jitsi-meet = {
+    enable = true;
+    hostName = "jitsi.example.com";
+  };
+  services.jitsi-videobridge.openFirewall = true;
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+  security.acme.email = "me@example.com";
+  security.acme.acceptTerms = true;
+}
+```
+
+## Configuration {#module-services-jitsi-configuration}
+
+Here is the minimal configuration with additional configurations:
+```
+{
+  services.jitsi-meet = {
+    enable = true;
+    hostName = "jitsi.example.com";
+    config = {
+      enableWelcomePage = false;
+      prejoinPageEnabled = true;
+      defaultLang = "fi";
+    };
+    interfaceConfig = {
+      SHOW_JITSI_WATERMARK = false;
+      SHOW_WATERMARK_FOR_GUESTS = false;
+    };
+  };
+  services.jitsi-videobridge.openFirewall = true;
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+  security.acme.email = "me@example.com";
+  security.acme.acceptTerms = true;
+}
+```
diff --git a/nixos/modules/services/web-apps/jitsi-meet.nix b/nixos/modules/services/web-apps/jitsi-meet.nix
index 5b0934b2fb7..3825b03c244 100644
--- a/nixos/modules/services/web-apps/jitsi-meet.nix
+++ b/nixos/modules/services/web-apps/jitsi-meet.nix
@@ -411,11 +411,14 @@ in
       componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret";
       bridgeMuc = "jvbbrewery@internal.${cfg.hostName}";
       config = mkMerge [{
-        "org.jitsi.jicofo.ALWAYS_TRUST_MODE_ENABLED" = "true";
+        jicofo.xmpp.service.disable-certificate-verification = true;
+        jicofo.xmpp.client.disable-certificate-verification = true;
       #} (lib.mkIf cfg.jibri.enable {
        } (lib.mkIf (config.services.jibri.enable || cfg.jibri.enable) {
-        "org.jitsi.jicofo.jibri.BREWERY" = "JibriBrewery@internal.${cfg.hostName}";
-        "org.jitsi.jicofo.jibri.PENDING_TIMEOUT" = "90";
+         jicofo.jibri = {
+           brewery-jid = "JibriBrewery@internal.${cfg.hostName}";
+           pending-timeout = "90";
+         };
       })];
     };
 
@@ -451,6 +454,6 @@ in
     };
   };
 
-  meta.doc = ./jitsi-meet.xml;
+  meta.doc = ./jitsi-meet.md;
   meta.maintainers = lib.teams.jitsi.members;
 }
diff --git a/nixos/modules/services/web-apps/jitsi-meet.xml b/nixos/modules/services/web-apps/jitsi-meet.xml
deleted file mode 100644
index ff44c724adf..00000000000
--- a/nixos/modules/services/web-apps/jitsi-meet.xml
+++ /dev/null
@@ -1,55 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-jitsi-meet">
- <title>Jitsi Meet</title>
- <para>
-   With Jitsi Meet on NixOS you can quickly configure a complete,
-   private, self-hosted video conferencing solution.
- </para>
-
- <section xml:id="module-services-jitsi-basic-usage">
- <title>Basic usage</title>
-   <para>
-     A minimal configuration using Let's Encrypt for TLS certificates looks like this:
-<programlisting>{
-  services.jitsi-meet = {
-    <link linkend="opt-services.jitsi-meet.enable">enable</link> = true;
-    <link linkend="opt-services.jitsi-meet.enable">hostName</link> = "jitsi.example.com";
-  };
-  <link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
-  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-  <link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
-  <link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
-}</programlisting>
-   </para>
- </section>
-
- <section xml:id="module-services-jitsi-configuration">
- <title>Configuration</title>
-   <para>
-     Here is the minimal configuration with additional configurations:
-<programlisting>{
-  services.jitsi-meet = {
-    <link linkend="opt-services.jitsi-meet.enable">enable</link> = true;
-    <link linkend="opt-services.jitsi-meet.enable">hostName</link> = "jitsi.example.com";
-    <link linkend="opt-services.jitsi-meet.config">config</link> = {
-      enableWelcomePage = false;
-      prejoinPageEnabled = true;
-      defaultLang = "fi";
-    };
-    <link linkend="opt-services.jitsi-meet.interfaceConfig">interfaceConfig</link> = {
-      SHOW_JITSI_WATERMARK = false;
-      SHOW_WATERMARK_FOR_GUESTS = false;
-    };
-  };
-  <link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
-  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-  <link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
-  <link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
-}</programlisting>
-   </para>
- </section>
-
-</chapter>
diff --git a/nixos/modules/services/web-apps/kasmweb/default.nix b/nixos/modules/services/web-apps/kasmweb/default.nix
new file mode 100644
index 00000000000..0d78025ecf0
--- /dev/null
+++ b/nixos/modules/services/web-apps/kasmweb/default.nix
@@ -0,0 +1,275 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.kasmweb;
+in
+{
+  options.services.kasmweb = {
+    enable = lib.mkEnableOption (lib.mdDoc "kasmweb");
+
+    networkSubnet = lib.mkOption {
+      default = "172.20.0.0/16";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        The network subnet to use for the containers.
+      '';
+    };
+
+    postgres = {
+      user = lib.mkOption {
+        default = "kasmweb";
+        type = lib.types.str;
+        description = lib.mdDoc ''
+          Username to use for the postgres database.
+        '';
+      };
+      password = lib.mkOption {
+        default = "kasmweb";
+        type = lib.types.str;
+        description = lib.mdDoc ''
+          password to use for the postgres database.
+        '';
+      };
+    };
+
+    redisPassword = lib.mkOption {
+      default = "kasmweb";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        password to use for the redis cache.
+      '';
+    };
+
+    defaultAdminPassword = lib.mkOption {
+      default = "kasmweb";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        default admin password to use.
+      '';
+    };
+
+    defaultUserPassword = lib.mkOption {
+      default = "kasmweb";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        default user password to use.
+      '';
+    };
+
+    defaultManagerToken = lib.mkOption {
+      default = "kasmweb";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        default manager token to use.
+      '';
+    };
+
+    defaultGuacToken = lib.mkOption {
+      default = "kasmweb";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        default guac token to use.
+      '';
+    };
+
+    defaultRegistrationToken = lib.mkOption {
+      default = "kasmweb";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        default registration token to use.
+      '';
+    };
+
+    datastorePath = lib.mkOption {
+      type = lib.types.str;
+      default = "/var/lib/kasmweb";
+      description = lib.mdDoc ''
+        The directory used to store all data for kasmweb.
+      '';
+    };
+
+    listenAddress = lib.mkOption {
+      type = lib.types.str;
+      default = "0.0.0.0";
+      description = lib.mdDoc ''
+        The address on which kasmweb should listen.
+      '';
+    };
+
+    listenPort = lib.mkOption {
+      type = lib.types.int;
+      default = 443;
+      description = lib.mdDoc ''
+        The port on which kasmweb should listen.
+      '';
+    };
+
+    sslCertificate = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = lib.mdDoc ''
+        The SSL certificate to be used for kasmweb.
+      '';
+    };
+
+    sslCertificateKey = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = lib.mdDoc ''
+        The SSL certificate's key to be used for kasmweb. Make sure to specify
+        this as a string and not a literal path, so that it is not accidentally
+        included in your nixstore.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    systemd.services = {
+      "init-kasmweb" = {
+        wantedBy = [
+          "docker-kasm_db.service"
+        ];
+        before = [
+          "docker-kasm_db.service"
+          "docker-kasm_redis.service"
+          "docker-kasm_db_init.service"
+          "docker-kasm_api.service"
+          "docker-kasm_agent.service"
+          "docker-kasm_manager.service"
+          "docker-kasm_share.service"
+          "docker-kasm_guac.service"
+          "docker-kasm_proxy.service"
+        ];
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = pkgs.substituteAll {
+            src = ./initialize_kasmweb.sh;
+            isExecutable = true;
+            binPath = lib.makeBinPath [ pkgs.docker pkgs.openssl pkgs.gnused ];
+            runtimeShell = pkgs.runtimeShell;
+            kasmweb = pkgs.kasmweb;
+            postgresUser = cfg.postgres.user;
+            postgresPassword = cfg.postgres.password;
+            inherit (cfg)
+              datastorePath
+              sslCertificate
+              sslCertificateKey
+              redisPassword
+              defaultUserPassword
+              defaultAdminPassword
+              defaultManagerToken
+              defaultRegistrationToken
+              defaultGuacToken;
+          };
+        };
+      };
+    };
+
+    virtualisation = {
+      oci-containers.containers = {
+        kasm_db = {
+          image = "postgres:12-alpine";
+          environment = {
+            POSTGRES_PASSWORD = cfg.postgres.password;
+            POSTGRES_USER = cfg.postgres.user;
+            POSTGRES_DB = "kasm";
+          };
+          volumes = [
+            "${cfg.datastorePath}/conf/database/data.sql:/docker-entrypoint-initdb.d/data.sql"
+            "${cfg.datastorePath}/conf/database/:/tmp/"
+            "kasmweb_db:/var/lib/postgresql/data"
+          ];
+          extraOptions = [ "--network=kasm_default_network" ];
+        };
+        kasm_db_init = {
+          image = "kasmweb/api:${pkgs.kasmweb.version}";
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/:/opt/kasm/current/"
+            "kasmweb_api_data:/tmp"
+          ];
+          dependsOn = [ "kasm_db" ];
+          entrypoint = "/bin/bash";
+          cmd = [ "/opt/kasm/current/init_seeds.sh" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host" ];
+        };
+        kasm_redis = {
+          image = "redis:5-alpine";
+          entrypoint = "/bin/sh";
+          cmd = [
+            "-c"
+            "redis-server --requirepass ${cfg.redisPassword}"
+          ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host" ];
+        };
+        kasm_api = {
+          image = "kasmweb/api:${pkgs.kasmweb.version}";
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/:/opt/kasm/current/"
+            "kasmweb_api_data:/tmp"
+          ];
+          dependsOn = [ "kasm_db_init" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host"  ];
+        };
+        kasm_manager = {
+          image = "kasmweb/manager:${pkgs.kasmweb.version}";
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/:/opt/kasm/current/"
+          ];
+          dependsOn = [ "kasm_db" "kasm_api" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host" "--read-only"];
+        };
+        kasm_agent = {
+          image = "kasmweb/agent:${pkgs.kasmweb.version}";
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/:/opt/kasm/current/"
+            "/var/run/docker.sock:/var/run/docker.sock"
+            "${pkgs.docker}/bin/docker:/usr/bin/docker"
+            "${cfg.datastorePath}/conf/nginx:/etc/nginx/conf.d"
+          ];
+          dependsOn = [ "kasm_manager" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host" "--read-only" ];
+        };
+        kasm_share = {
+          image = "kasmweb/share:${pkgs.kasmweb.version}";
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/:/opt/kasm/current/"
+          ];
+          dependsOn = [ "kasm_db" "kasm_redis" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host" "--read-only" ];
+        };
+        kasm_guac = {
+          image = "kasmweb/kasm-guac:${pkgs.kasmweb.version}";
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/:/opt/kasm/current/"
+          ];
+          dependsOn = [ "kasm_db" "kasm_redis" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host" "--read-only" ];
+        };
+        kasm_proxy = {
+          image = "kasmweb/nginx:latest";
+          ports = [ "${cfg.listenAddress}:${toString cfg.listenPort}:443" ];
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/conf/nginx:/etc/nginx/conf.d:ro"
+            "${cfg.datastorePath}/certs/kasm_nginx.key:/etc/ssl/private/kasm_nginx.key"
+            "${cfg.datastorePath}/certs/kasm_nginx.crt:/etc/ssl/certs/kasm_nginx.crt"
+            "${cfg.datastorePath}/www:/srv/www:ro"
+            "${cfg.datastorePath}/log/nginx:/var/log/external/nginx"
+            "${cfg.datastorePath}/log/logrotate:/var/log/external/logrotate"
+          ];
+          dependsOn = [ "kasm_manager" "kasm_api" "kasm_agent" "kasm_share"
+          "kasm_guac" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host"
+          "--network-alias=proxy"];
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/kasmweb/initialize_kasmweb.sh b/nixos/modules/services/web-apps/kasmweb/initialize_kasmweb.sh
new file mode 100644
index 00000000000..dbf043b9869
--- /dev/null
+++ b/nixos/modules/services/web-apps/kasmweb/initialize_kasmweb.sh
@@ -0,0 +1,114 @@
+#! @runtimeShell@
+export PATH=@binPath@:$PATH
+
+mkdir -p @datastorePath@/log
+chmod -R a+rw @datastorePath@
+
+ln -sf @kasmweb@/bin @datastorePath@
+rm -r @datastorePath@/conf
+cp -r @kasmweb@/conf @datastorePath@
+mkdir -p @datastorePath@/conf/nginx/containers.d
+chmod -R a+rw @datastorePath@/conf
+ln -sf @kasmweb@/www @datastorePath@
+
+
+docker network inspect kasm_default_network >/dev/null || docker network create kasm_default_network --subnet @networkSubnet@
+if docker volume inspect kasmweb_db >/dev/null; then
+    source @datastorePath@/ids.env
+    echo 'echo "skipping database init"' > @datastorePath@/init_seeds.sh
+    echo 'while true; do sleep 10 ; done' >> @datastorePath@/init_seeds.sh
+else
+    API_SERVER_ID=$(cat /proc/sys/kernel/random/uuid)
+    MANAGER_ID=$(cat /proc/sys/kernel/random/uuid)
+    SHARE_ID=$(cat /proc/sys/kernel/random/uuid)
+    SERVER_ID=$(cat /proc/sys/kernel/random/uuid)
+    echo "export API_SERVER_ID=$API_SERVER_ID" > @datastorePath@/ids.env
+    echo "export MANAGER_ID=$MANAGER_ID" >> @datastorePath@/ids.env
+    echo "export SHARE_ID=$SHARE_ID" >> @datastorePath@/ids.env
+    echo "export SERVER_ID=$SERVER_ID" >> @datastorePath@/ids.env
+
+    mkdir -p @datastorePath@/certs
+    openssl req -x509 -nodes -days 1825 -newkey rsa:2048 -keyout @datastorePath@/certs/kasm_nginx.key -out @datastorePath@/certs/kasm_nginx.crt -subj "/C=US/ST=VA/L=None/O=None/OU=DoFu/CN=$(hostname)/emailAddress=none@none.none" 2> /dev/null
+
+    docker volume create kasmweb_db
+    rm @datastorePath@/.done_initing_data
+    cat >@datastorePath@/init_seeds.sh <<EOF
+#!/bin/bash
+if [ ! -e /opt/kasm/current/.done_initing_data ]; then
+  sleep 4
+  /usr/bin/kasm_server.so --initialize-database --cfg \
+    /opt/kasm/current/conf/app/api.app.config.yaml \
+    --populate-production \
+    --seed-file \
+    /opt/kasm/current/conf/database/seed_data/default_properties.yaml \
+    2>&1 | grep -v UserWarning
+  /usr/bin/kasm_server.so --cfg \
+    /opt/kasm/current/conf/app/api.app.config.yaml \
+    --populate-production \
+    --seed-file \
+    /opt/kasm/current/conf/database/seed_data/default_agents.yaml \
+    2>&1 | grep -v UserWarning
+  /usr/bin/kasm_server.so --cfg \
+    /opt/kasm/current/conf/app/api.app.config.yaml \
+    --populate-production \
+    --seed-file \
+    /opt/kasm/current/conf/database/seed_data/default_connection_proxies.yaml \
+    2>&1 | grep -v UserWarning
+  /usr/bin/kasm_server.so --cfg \
+    /opt/kasm/current/conf/app/api.app.config.yaml \
+    --populate-production \
+    --seed-file \
+    /opt/kasm/current/conf/database/seed_data/default_images_amd64.yaml \
+    2>&1 | grep -v UserWarning
+  touch /opt/kasm/current/.done_initing_data
+  while true; do sleep 10 ; done
+else
+ echo "skipping database init"
+  while true; do sleep 10 ; done
+fi
+EOF
+fi
+
+chmod +x @datastorePath@/init_seeds.sh
+chmod a+w @datastorePath@/init_seeds.sh
+
+if [ -e @sslCertificate@ ]; then
+    cp @sslCertificate@ @datastorePath@/certs/kasm_nginx.crt
+    cp @sslCertificateKey@ @datastorePath@/certs/kasm_nginx.key
+fi
+
+sed -i -e "s/username.*/username: @postgresUser@/g" \
+    -e "s/password.*/password: @postgresPassword@/g" \
+    -e "s/host.*db/host: kasm_db/g" \
+    -e "s/ssl: true/ssl: false/g" \
+    -e "s/redisPassword.*/redisPassword: @redisPassword@/g" \
+    -e "s/server_hostname.*/server_hostname: kasm_api/g" \
+    -e "s/server_id.*/server_id: $API_SERVER_ID/g" \
+    -e "s/manager_id.*/manager_id: $MANAGER_ID/g" \
+    -e "s/share_id.*/share_id: $SHARE_ID/g" \
+    @datastorePath@/conf/app/api.app.config.yaml
+
+sed -i -e "s/ token:.*/ token: \"@defaultManagerToken@\"/g" \
+    -e "s/hostnames: \['proxy.*/hostnames: \['kasm_proxy'\]/g" \
+    -e "s/server_id.*/server_id: $SERVER_ID/g" \
+    @datastorePath@/conf/app/agent.app.config.yaml
+
+
+sed -i -e "s/password: admin.*/password: \"@defaultAdminPassword@\"/g" \
+    -e "s/password: user.*/password: \"@defaultUserPassword@\"/g" \
+    -e "s/default-manager-token/@defaultManagerToken@/g" \
+    -e "s/default-registration-token/@defaultRegistrationToken@/g" \
+    -e "s/upstream_auth_address:.*/upstream_auth_address: 'proxy'/g" \
+    @datastorePath@/conf/database/seed_data/default_properties.yaml
+
+sed -i -e "s/GUACTOKEN/@defaultGuacToken@/g" \
+    -e "s/APIHOSTNAME/proxy/g" \
+    @datastorePath@/conf/app/kasmguac.app.config.yaml
+
+sed -i -e "s/GUACTOKEN/@defaultGuacToken@/g" \
+    -e "s/APIHOSTNAME/proxy/g" \
+    @datastorePath@/conf/database/seed_data/default_connection_proxies.yaml
+
+sed -i "s/00000000-0000-0000-0000-000000000000/$SERVER_ID/g" \
+    @datastorePath@/conf/database/seed_data/default_agents.yaml
+
diff --git a/nixos/modules/services/web-apps/kavita.nix b/nixos/modules/services/web-apps/kavita.nix
new file mode 100644
index 00000000000..ca9cd01d403
--- /dev/null
+++ b/nixos/modules/services/web-apps/kavita.nix
@@ -0,0 +1,83 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.kavita;
+in {
+  options.services.kavita = {
+    enable = lib.mkEnableOption (lib.mdDoc "Kavita reading server");
+
+    user = lib.mkOption {
+      type = lib.types.str;
+      default = "kavita";
+      description = lib.mdDoc "User account under which Kavita runs.";
+    };
+
+    package = lib.mkPackageOptionMD pkgs "kavita" { };
+
+    dataDir = lib.mkOption {
+      default = "/var/lib/kavita";
+      type = lib.types.str;
+      description = lib.mdDoc "The directory where Kavita stores its state.";
+    };
+
+    tokenKeyFile = lib.mkOption {
+      type = lib.types.path;
+      description = lib.mdDoc ''
+        A file containing the TokenKey, a secret with at 128+ bits.
+        It can be generated with `head -c 32 /dev/urandom | base64`.
+      '';
+    };
+    port = lib.mkOption {
+      default = 5000;
+      type = lib.types.port;
+      description = lib.mdDoc "Port to bind to.";
+    };
+    ipAdresses = lib.mkOption {
+      default = ["0.0.0.0" "::"];
+      type = lib.types.listOf lib.types.str;
+      description = lib.mdDoc "IP Addresses to bind to. The default is to bind
+      to all IPv4 and IPv6 addresses.";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.kavita = {
+      description = "Kavita";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      preStart = ''
+        umask u=rwx,g=rx,o=
+        cat > "${cfg.dataDir}/config/appsettings.json" <<EOF
+        {
+          "TokenKey": "$(cat ${cfg.tokenKeyFile})",
+          "Port": ${toString cfg.port},
+          "IpAddresses": "${lib.concatStringsSep "," cfg.ipAdresses}"
+        }
+        EOF
+      '';
+      serviceConfig = {
+        WorkingDirectory = cfg.dataDir;
+        ExecStart = "${lib.getExe cfg.package}";
+        Restart = "always";
+        User = cfg.user;
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}'        0750 ${cfg.user} ${cfg.user} - -"
+      "d '${cfg.dataDir}/config' 0750 ${cfg.user} ${cfg.user} - -"
+    ];
+
+    users = {
+      users.${cfg.user} = {
+        description = "kavita service user";
+        isSystemUser = true;
+        group = cfg.user;
+        home = cfg.dataDir;
+      };
+      groups.${cfg.user} = { };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ misterio77 ];
+}
diff --git a/nixos/modules/services/web-apps/keycloak.md b/nixos/modules/services/web-apps/keycloak.md
new file mode 100644
index 00000000000..aa8de40d642
--- /dev/null
+++ b/nixos/modules/services/web-apps/keycloak.md
@@ -0,0 +1,141 @@
+# Keycloak {#module-services-keycloak}
+
+[Keycloak](https://www.keycloak.org/) is an
+open source identity and access management server with support for
+[OpenID Connect](https://openid.net/connect/),
+[OAUTH 2.0](https://oauth.net/2/) and
+[SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0).
+
+## Administration {#module-services-keycloak-admin}
+
+An administrative user with the username
+`admin` is automatically created in the
+`master` realm. Its initial password can be
+configured by setting [](#opt-services.keycloak.initialAdminPassword)
+and defaults to `changeme`. The password is
+not stored safely and should be changed immediately in the
+admin panel.
+
+Refer to the [Keycloak Server Administration Guide](
+  https://www.keycloak.org/docs/latest/server_admin/index.html
+) for information on
+how to administer your Keycloak
+instance.
+
+## Database access {#module-services-keycloak-database}
+
+Keycloak can be used with either PostgreSQL, MariaDB or
+MySQL. Which one is used can be
+configured in [](#opt-services.keycloak.database.type). The selected
+database will automatically be enabled and a database and role
+created unless [](#opt-services.keycloak.database.host) is changed
+from its default of `localhost` or
+[](#opt-services.keycloak.database.createLocally) is set to `false`.
+
+External database access can also be configured by setting
+[](#opt-services.keycloak.database.host),
+[](#opt-services.keycloak.database.name),
+[](#opt-services.keycloak.database.username),
+[](#opt-services.keycloak.database.useSSL) and
+[](#opt-services.keycloak.database.caCert) as
+appropriate. Note that you need to manually create the database
+and allow the configured database user full access to it.
+
+[](#opt-services.keycloak.database.passwordFile)
+must be set to the path to a file containing the password used
+to log in to the database. If [](#opt-services.keycloak.database.host)
+and [](#opt-services.keycloak.database.createLocally)
+are kept at their defaults, the database role
+`keycloak` with that password is provisioned
+on the local database instance.
+
+::: {.warning}
+The path should be provided as a string, not a Nix path, since Nix
+paths are copied into the world readable Nix store.
+:::
+
+## Hostname {#module-services-keycloak-hostname}
+
+The hostname is used to build the public URL used as base for
+all frontend requests and must be configured through
+[](#opt-services.keycloak.settings.hostname).
+
+::: {.note}
+If you're migrating an old Wildfly based Keycloak instance
+and want to keep compatibility with your current clients,
+you'll likely want to set [](#opt-services.keycloak.settings.http-relative-path)
+to `/auth`. See the option description
+for more details.
+:::
+
+[](#opt-services.keycloak.settings.hostname-strict-backchannel)
+determines whether Keycloak should force all requests to go
+through the frontend URL. By default,
+Keycloak allows backend requests to
+instead use its local hostname or IP address and may also
+advertise it to clients through its OpenID Connect Discovery
+endpoint.
+
+For more information on hostname configuration, see the [Hostname
+section of the Keycloak Server Installation and Configuration
+Guide](https://www.keycloak.org/server/hostname).
+
+## Setting up TLS/SSL {#module-services-keycloak-tls}
+
+By default, Keycloak won't accept
+unsecured HTTP connections originating from outside its local
+network.
+
+HTTPS support requires a TLS/SSL certificate and a private key,
+both [PEM formatted](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail).
+Their paths should be set through
+[](#opt-services.keycloak.sslCertificate) and
+[](#opt-services.keycloak.sslCertificateKey).
+
+::: {.warning}
+ The paths should be provided as a strings, not a Nix paths,
+since Nix paths are copied into the world readable Nix store.
+:::
+
+## Themes {#module-services-keycloak-themes}
+
+You can package custom themes and make them visible to
+Keycloak through [](#opt-services.keycloak.themes). See the
+[Themes section of the Keycloak Server Development Guide](
+  https://www.keycloak.org/docs/latest/server_development/#_themes
+) and the description of the aforementioned NixOS option for
+more information.
+
+## Configuration file settings {#module-services-keycloak-settings}
+
+Keycloak server configuration parameters can be set in
+[](#opt-services.keycloak.settings). These correspond
+directly to options in
+{file}`conf/keycloak.conf`. Some of the most
+important parameters are documented as suboptions, the rest can
+be found in the [All
+configuration section of the Keycloak Server Installation and
+Configuration Guide](https://www.keycloak.org/server/all-config).
+
+Options containing secret data should be set to an attribute
+set containing the attribute `_secret` - a
+string pointing to a file containing the value the option
+should be set to. See the description of
+[](#opt-services.keycloak.settings) for an example.
+
+## Example configuration {#module-services-keycloak-example-config}
+
+A basic configuration with some custom settings could look like this:
+```
+services.keycloak = {
+  enable = true;
+  settings = {
+    hostname = "keycloak.example.com";
+    hostname-strict-backchannel = true;
+  };
+  initialAdminPassword = "e6Wcm0RrtegMEHl";  # change on first login
+  sslCertificate = "/run/keys/ssl_cert";
+  sslCertificateKey = "/run/keys/ssl_key";
+  database.passwordFile = "/run/keys/db_password";
+};
+```
diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
index d52190a2864..a7e4fab8ea2 100644
--- a/nixos/modules/services/web-apps/keycloak.nix
+++ b/nixos/modules/services/web-apps/keycloak.nix
@@ -674,6 +674,6 @@ in
           mkIf createLocalMySQL (mkDefault dbPkg);
       };
 
-  meta.doc = ./keycloak.xml;
+  meta.doc = ./keycloak.md;
   meta.maintainers = [ maintainers.talyz ];
 }
diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml
deleted file mode 100644
index 861756e33ac..00000000000
--- a/nixos/modules/services/web-apps/keycloak.xml
+++ /dev/null
@@ -1,202 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-keycloak">
- <title>Keycloak</title>
- <para>
-   <link xlink:href="https://www.keycloak.org/">Keycloak</link> is an
-   open source identity and access management server with support for
-   <link xlink:href="https://openid.net/connect/">OpenID
-   Connect</link>, <link xlink:href="https://oauth.net/2/">OAUTH
-   2.0</link> and <link
-   xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML
-   2.0</link>.
- </para>
-   <section xml:id="module-services-keycloak-admin">
-     <title>Administration</title>
-     <para>
-       An administrative user with the username
-       <literal>admin</literal> is automatically created in the
-       <literal>master</literal> realm. Its initial password can be
-       configured by setting <xref linkend="opt-services.keycloak.initialAdminPassword" />
-       and defaults to <literal>changeme</literal>. The password is
-       not stored safely and should be changed immediately in the
-       admin panel.
-     </para>
-
-     <para>
-       Refer to the <link
-       xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html">
-       Keycloak Server Administration Guide</link> for information on
-       how to administer your <productname>Keycloak</productname>
-       instance.
-     </para>
-   </section>
-
-   <section xml:id="module-services-keycloak-database">
-     <title>Database access</title>
-     <para>
-       <productname>Keycloak</productname> can be used with either
-       <productname>PostgreSQL</productname>,
-       <productname>MariaDB</productname> or
-       <productname>MySQL</productname>. Which one is used can be
-       configured in <xref
-       linkend="opt-services.keycloak.database.type" />. The selected
-       database will automatically be enabled and a database and role
-       created unless <xref
-       linkend="opt-services.keycloak.database.host" /> is changed
-       from its default of <literal>localhost</literal> or <xref
-       linkend="opt-services.keycloak.database.createLocally" /> is
-       set to <literal>false</literal>.
-     </para>
-
-     <para>
-       External database access can also be configured by setting
-       <xref linkend="opt-services.keycloak.database.host" />, <xref
-       linkend="opt-services.keycloak.database.name" />, <xref
-       linkend="opt-services.keycloak.database.username" />, <xref
-       linkend="opt-services.keycloak.database.useSSL" /> and <xref
-       linkend="opt-services.keycloak.database.caCert" /> as
-       appropriate. Note that you need to manually create the database
-       and allow the configured database user full access to it.
-     </para>
-
-     <para>
-       <xref linkend="opt-services.keycloak.database.passwordFile" />
-       must be set to the path to a file containing the password used
-       to log in to the database. If <xref linkend="opt-services.keycloak.database.host" />
-       and <xref linkend="opt-services.keycloak.database.createLocally" />
-       are kept at their defaults, the database role
-       <literal>keycloak</literal> with that password is provisioned
-       on the local database instance.
-     </para>
-
-     <warning>
-       <para>
-         The path should be provided as a string, not a Nix path, since Nix
-         paths are copied into the world readable Nix store.
-       </para>
-     </warning>
-   </section>
-
-   <section xml:id="module-services-keycloak-hostname">
-     <title>Hostname</title>
-     <para>
-       The hostname is used to build the public URL used as base for
-       all frontend requests and must be configured through <xref
-       linkend="opt-services.keycloak.settings.hostname" />.
-     </para>
-
-     <note>
-       <para>
-         If you're migrating an old Wildfly based Keycloak instance
-         and want to keep compatibility with your current clients,
-         you'll likely want to set <xref
-         linkend="opt-services.keycloak.settings.http-relative-path"
-         /> to <literal>/auth</literal>. See the option description
-         for more details.
-       </para>
-     </note>
-
-     <para>
-       <xref linkend="opt-services.keycloak.settings.hostname-strict-backchannel" />
-       determines whether Keycloak should force all requests to go
-       through the frontend URL. By default,
-       <productname>Keycloak</productname> allows backend requests to
-       instead use its local hostname or IP address and may also
-       advertise it to clients through its OpenID Connect Discovery
-       endpoint.
-     </para>
-
-     <para>
-        For more information on hostname configuration, see the <link
-        xlink:href="https://www.keycloak.org/server/hostname">Hostname
-        section of the Keycloak Server Installation and Configuration
-        Guide</link>.
-     </para>
-   </section>
-
-   <section xml:id="module-services-keycloak-tls">
-     <title>Setting up TLS/SSL</title>
-     <para>
-       By default, <productname>Keycloak</productname> won't accept
-       unsecured HTTP connections originating from outside its local
-       network.
-     </para>
-
-     <para>
-       HTTPS support requires a TLS/SSL certificate and a private key,
-       both <link
-       xlink:href="https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail">PEM
-       formatted</link>. Their paths should be set through <xref
-       linkend="opt-services.keycloak.sslCertificate" /> and <xref
-       linkend="opt-services.keycloak.sslCertificateKey" />.
-     </para>
-
-     <warning>
-       <para>
-         The paths should be provided as a strings, not a Nix paths,
-         since Nix paths are copied into the world readable Nix store.
-       </para>
-     </warning>
-   </section>
-
-   <section xml:id="module-services-keycloak-themes">
-     <title>Themes</title>
-     <para>
-        You can package custom themes and make them visible to
-        Keycloak through <xref linkend="opt-services.keycloak.themes"
-        />. See the <link
-        xlink:href="https://www.keycloak.org/docs/latest/server_development/#_themes">
-        Themes section of the Keycloak Server Development Guide</link>
-        and the description of the aforementioned NixOS option for
-        more information.
-     </para>
-   </section>
-
-   <section xml:id="module-services-keycloak-settings">
-     <title>Configuration file settings</title>
-     <para>
-       Keycloak server configuration parameters can be set in <xref
-       linkend="opt-services.keycloak.settings" />. These correspond
-       directly to options in
-       <filename>conf/keycloak.conf</filename>. Some of the most
-       important parameters are documented as suboptions, the rest can
-       be found in the <link
-       xlink:href="https://www.keycloak.org/server/all-config">All
-       configuration section of the Keycloak Server Installation and
-       Configuration Guide</link>.
-     </para>
-
-     <para>
-       Options containing secret data should be set to an attribute
-       set containing the attribute <literal>_secret</literal> - a
-       string pointing to a file containing the value the option
-       should be set to. See the description of <xref
-       linkend="opt-services.keycloak.settings" /> for an example.
-     </para>
-   </section>
-
-
-   <section xml:id="module-services-keycloak-example-config">
-     <title>Example configuration</title>
-     <para>
-       A basic configuration with some custom settings could look like this:
-<programlisting>
-services.keycloak = {
-  <link linkend="opt-services.keycloak.enable">enable</link> = true;
-  settings = {
-    <link linkend="opt-services.keycloak.settings.hostname">hostname</link> = "keycloak.example.com";
-    <link linkend="opt-services.keycloak.settings.hostname-strict-backchannel">hostname-strict-backchannel</link> = true;
-  };
-  <link linkend="opt-services.keycloak.initialAdminPassword">initialAdminPassword</link> = "e6Wcm0RrtegMEHl";  # change on first login
-  <link linkend="opt-services.keycloak.sslCertificate">sslCertificate</link> = "/run/keys/ssl_cert";
-  <link linkend="opt-services.keycloak.sslCertificateKey">sslCertificateKey</link> = "/run/keys/ssl_key";
-  <link linkend="opt-services.keycloak.database.passwordFile">database.passwordFile</link> = "/run/keys/db_password";
-};
-</programlisting>
-     </para>
-
-   </section>
- </chapter>
diff --git a/nixos/modules/services/web-apps/lemmy.nix b/nixos/modules/services/web-apps/lemmy.nix
index 267584dd0ca..7662478f68b 100644
--- a/nixos/modules/services/web-apps/lemmy.nix
+++ b/nixos/modules/services/web-apps/lemmy.nix
@@ -6,9 +6,7 @@ let
 in
 {
   meta.maintainers = with maintainers; [ happysalada ];
-  # Don't edit the docbook xml directly, edit the md and generate it:
-  # `pandoc lemmy.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > lemmy.xml`
-  meta.doc = ./lemmy.xml;
+  meta.doc = ./lemmy.md;
 
   imports = [
     (mkRemovedOptionModule [ "services" "lemmy" "jwtSecretPath" ] "As of v0.13.0, Lemmy auto-generates the JWT secret.")
@@ -18,7 +16,13 @@ in
 
     enable = mkEnableOption (lib.mdDoc "lemmy a federated alternative to reddit in rust");
 
+    server = {
+      package = mkPackageOptionMD pkgs "lemmy-server" {};
+    };
+
     ui = {
+      package = mkPackageOptionMD pkgs "lemmy-ui" {};
+
       port = mkOption {
         type = types.port;
         default = 1234;
@@ -27,8 +31,17 @@ in
     };
 
     caddy.enable = mkEnableOption (lib.mdDoc "exposing lemmy with the caddy reverse proxy");
+    nginx.enable = mkEnableOption (lib.mdDoc "exposing lemmy with the nginx reverse proxy");
 
-    database.createLocally = mkEnableOption (lib.mdDoc "creation of database on the instance");
+    database = {
+      createLocally = mkEnableOption (lib.mdDoc "creation of database on the instance");
+
+      uri = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = lib.mdDoc "The connection URI to use. Takes priority over the configuration file if set.";
+      };
+    };
 
     settings = mkOption {
       default = { };
@@ -49,10 +62,6 @@ in
           description = lib.mdDoc "Port where lemmy should listen for incoming requests.";
         };
 
-        options.federation = {
-          enabled = mkEnableOption (lib.mdDoc "activitypub federation");
-        };
-
         options.captcha = {
           enabled = mkOption {
             type = types.bool;
@@ -68,6 +77,11 @@ in
       };
     };
 
+    secretFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      description = lib.mdDoc "Path to a secret JSON configuration file which is merged at runtime with the one generated from {option}`services.lemmy.settings`.";
+    };
   };
 
   config =
@@ -113,7 +127,7 @@ in
         virtualHosts."${cfg.settings.hostname}" = {
           extraConfig = ''
             handle_path /static/* {
-              root * ${pkgs.lemmy-ui}/dist
+              root * ${cfg.ui.package}/dist
               file_server
             }
             @for_backend {
@@ -142,24 +156,66 @@ in
         };
       };
 
-      assertions = [{
-        assertion = cfg.database.createLocally -> cfg.settings.database.host == "localhost" || cfg.settings.database.host == "/run/postgresql";
-        message = "if you want to create the database locally, you need to use a local database";
-      }];
+      services.nginx = mkIf cfg.nginx.enable {
+        enable = mkDefault true;
+        virtualHosts."${cfg.settings.hostname}".locations = let
+          ui = "http://127.0.0.1:${toString cfg.ui.port}";
+          backend = "http://127.0.0.1:${toString cfg.settings.port}";
+        in {
+          "~ ^/(api|pictrs|feeds|nodeinfo|.well-known)" = {
+            # backend requests
+            proxyPass = backend;
+            proxyWebsockets = true;
+            recommendedProxySettings = true;
+          };
+          "/" = {
+            # mixed frontend and backend requests, based on the request headers
+            proxyPass = "$proxpass";
+            recommendedProxySettings = true;
+            extraConfig = ''
+              set $proxpass "${ui}";
+              if ($http_accept = "application/activity+json") {
+                set $proxpass "${backend}";
+              }
+              if ($http_accept = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") {
+                set $proxpass "${backend}";
+              }
+              if ($request_method = POST) {
+                set $proxpass "${backend}";
+              }
+
+              # Cuts off the trailing slash on URLs to make them valid
+              rewrite ^(.+)/+$ $1 permanent;
+            '';
+          };
+        };
+      };
 
-      systemd.services.lemmy = {
+      assertions = [
+        {
+          assertion = cfg.database.createLocally -> cfg.settings.database.host == "localhost" || cfg.settings.database.host == "/run/postgresql";
+          message = "if you want to create the database locally, you need to use a local database";
+        }
+        {
+          assertion = (!(hasAttrByPath ["federation"] cfg.settings)) && (!(hasAttrByPath ["federation" "enabled"] cfg.settings));
+          message = "`services.lemmy.settings.federation` was removed in 0.17.0 and no longer has any effect";
+        }
+      ];
+
+      systemd.services.lemmy = let
+        configFile = settingsFormat.generate "config.hjson" cfg.settings;
+        mergedConfig = "/run/lemmy/config.hjson";
+      in {
         description = "Lemmy server";
 
         environment = {
-          LEMMY_CONFIG_LOCATION = "/run/lemmy/config.hjson";
-
-          # Verify how this is used, and don't put the password in the nix store
-          LEMMY_DATABASE_URL = with cfg.settings.database;"postgres:///${database}?host=${host}";
+          LEMMY_CONFIG_LOCATION = if cfg.secretFile == null then configFile else mergedConfig;
+          LEMMY_DATABASE_URL = if cfg.database.uri != null then cfg.database.uri else (mkIf (cfg.database.createLocally) "postgres:///lemmy?host=/run/postgresql&user=lemmy");
         };
 
         documentation = [
-          "https://join-lemmy.org/docs/en/administration/from_scratch.html"
-          "https://join-lemmy.org/docs"
+          "https://join-lemmy.org/docs/en/admins/from_scratch.html"
+          "https://join-lemmy.org/docs/en/"
         ];
 
         wantedBy = [ "multi-user.target" ];
@@ -168,11 +224,24 @@ in
 
         requires = lib.optionals cfg.database.createLocally [ "postgresql.service" ];
 
+        path = mkIf (cfg.secretFile != null) [ pkgs.jq ];
+
+        # merge the two configs and prevent others from reading the result
+        # if somehow $CREDENTIALS_DIRECTORY is not set we fail
+        preStart = mkIf (cfg.secretFile != null) ''
+          set -u
+          umask 177
+          jq --slurp '.[0] * .[1]' ${lib.escapeShellArg configFile} "$CREDENTIALS_DIRECTORY/secretFile" > ${lib.escapeShellArg mergedConfig}
+        '';
+
         serviceConfig = {
           DynamicUser = true;
           RuntimeDirectory = "lemmy";
-          ExecStartPre = "${pkgs.coreutils}/bin/install -m 600 ${settingsFormat.generate "config.hjson" cfg.settings} /run/lemmy/config.hjson";
-          ExecStart = "${pkgs.lemmy-server}/bin/lemmy_server";
+          ExecStart = "${cfg.server.package}/bin/lemmy_server";
+          LoadCredential = mkIf (cfg.secretFile != null) "secretFile:${toString cfg.secretFile}";
+          PrivateTmp = true;
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
         };
       };
 
@@ -181,14 +250,14 @@ in
 
         environment = {
           LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}";
-          LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}";
-          LEMMY_EXTERNAL_HOST = cfg.settings.hostname;
-          LEMMY_HTTPS = "false";
+          LEMMY_UI_LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}";
+          LEMMY_UI_LEMMY_EXTERNAL_HOST = cfg.settings.hostname;
+          LEMMY_UI_HTTPS = "false";
         };
 
         documentation = [
-          "https://join-lemmy.org/docs/en/administration/from_scratch.html"
-          "https://join-lemmy.org/docs"
+          "https://join-lemmy.org/docs/en/admins/from_scratch.html"
+          "https://join-lemmy.org/docs/en/"
         ];
 
         wantedBy = [ "multi-user.target" ];
@@ -199,8 +268,8 @@ in
 
         serviceConfig = {
           DynamicUser = true;
-          WorkingDirectory = "${pkgs.lemmy-ui}";
-          ExecStart = "${pkgs.nodejs}/bin/node ${pkgs.lemmy-ui}/dist/js/server.js";
+          WorkingDirectory = "${cfg.ui.package}";
+          ExecStart = "${pkgs.nodejs}/bin/node ${cfg.ui.package}/dist/js/server.js";
         };
       };
     };
diff --git a/nixos/modules/services/web-apps/lemmy.xml b/nixos/modules/services/web-apps/lemmy.xml
deleted file mode 100644
index f04316b3c51..00000000000
--- a/nixos/modules/services/web-apps/lemmy.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-lemmy">
-  <title>Lemmy</title>
-  <para>
-    Lemmy is a federated alternative to reddit in rust.
-  </para>
-  <section xml:id="module-services-lemmy-quickstart">
-    <title>Quickstart</title>
-    <para>
-      the minimum to start lemmy is
-    </para>
-    <programlisting language="nix">
-services.lemmy = {
-  enable = true;
-  settings = {
-    hostname = &quot;lemmy.union.rocks&quot;;
-    database.createLocally = true;
-  };
-  caddy.enable = true;
-}
-</programlisting>
-    <para>
-      this will start the backend on port 8536 and the frontend on port
-      1234. It will expose your instance with a caddy reverse proxy to
-      the hostname you’ve provided. Postgres will be initialized on that
-      same instance automatically.
-    </para>
-  </section>
-  <section xml:id="module-services-lemmy-usage">
-    <title>Usage</title>
-    <para>
-      On first connection you will be asked to define an admin user.
-    </para>
-  </section>
-  <section xml:id="module-services-lemmy-missing">
-    <title>Missing</title>
-    <itemizedlist spacing="compact">
-      <listitem>
-        <para>
-          Exposing with nginx is not implemented yet.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          This has been tested using a local database with a unix socket
-          connection. Using different database settings will likely
-          require modifications
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/limesurvey.nix b/nixos/modules/services/web-apps/limesurvey.nix
index 7093d1de0da..920e6928ef5 100644
--- a/nixos/modules/services/web-apps/limesurvey.nix
+++ b/nixos/modules/services/web-apps/limesurvey.nix
@@ -32,7 +32,25 @@ in
   # interface
 
   options.services.limesurvey = {
-    enable = mkEnableOption (lib.mdDoc "Limesurvey web application.");
+    enable = mkEnableOption (lib.mdDoc "Limesurvey web application");
+
+    encryptionKey = mkOption {
+      type = types.str;
+      default = "E17687FC77CEE247F0E22BB3ECF27FDE8BEC310A892347EC13013ABA11AA7EB5";
+      description = lib.mdDoc ''
+        This is a 32-byte key used to encrypt variables in the database.
+        You _must_ change this from the default value.
+      '';
+    };
+
+    encryptionNonce = mkOption {
+      type = types.str;
+      default = "1ACC8555619929DB91310BE848025A427B0F364A884FFA77";
+      description = lib.mdDoc ''
+        This is a 24-byte nonce used to encrypt variables in the database.
+        You _must_ change this from the default value.
+      '';
+    };
 
     database = {
       type = mkOption {
@@ -42,6 +60,12 @@ in
         description = lib.mdDoc "Database engine to use.";
       };
 
+      dbEngine = mkOption {
+        type = types.enum [ "MyISAM" "InnoDB" ];
+        default = "InnoDB";
+        description = lib.mdDoc "Database storage engine to use.";
+      };
+
       host = mkOption {
         type = types.str;
         default = "localhost";
@@ -180,6 +204,8 @@ in
       config = {
         tempdir = "${stateDir}/tmp";
         uploaddir = "${stateDir}/upload";
+        encryptionnonce = cfg.encryptionNonce;
+        encryptionsecretboxkey = cfg.encryptionKey;
         force_ssl = mkIf (cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL) "on";
         config.defaultlang = "en";
       };
@@ -200,6 +226,8 @@ in
 
     services.phpfpm.pools.limesurvey = {
       inherit user group;
+      phpPackage = pkgs.php81;
+      phpEnv.DBENGINE = "${cfg.database.dbEngine}";
       phpEnv.LIMESURVEY_CONFIG = "${limesurveyConfig}";
       settings = {
         "listen.owner" = config.services.httpd.user;
@@ -256,11 +284,12 @@ in
       wantedBy = [ "multi-user.target" ];
       before = [ "phpfpm-limesurvey.service" ];
       after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+      environment.DBENGINE = "${cfg.database.dbEngine}";
       environment.LIMESURVEY_CONFIG = limesurveyConfig;
       script = ''
         # update or install the database as required
-        ${pkgs.php}/bin/php ${pkg}/share/limesurvey/application/commands/console.php updatedb || \
-        ${pkgs.php}/bin/php ${pkg}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose
+        ${pkgs.php81}/bin/php ${pkg}/share/limesurvey/application/commands/console.php updatedb || \
+        ${pkgs.php81}/bin/php ${pkg}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose
       '';
       serviceConfig = {
         User = user;
diff --git a/nixos/modules/services/web-apps/mainsail.nix b/nixos/modules/services/web-apps/mainsail.nix
new file mode 100644
index 00000000000..f335d9b015d
--- /dev/null
+++ b/nixos/modules/services/web-apps/mainsail.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.mainsail;
+  moonraker = config.services.moonraker;
+in
+{
+  options.services.mainsail = {
+    enable = mkEnableOption (lib.mdDoc "a modern and responsive user interface for Klipper");
+
+    package = mkOption {
+      type = types.package;
+      description = lib.mdDoc "Mainsail package to be used in the module";
+      default = pkgs.mainsail;
+      defaultText = literalExpression "pkgs.mainsail";
+    };
+
+    hostName = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = lib.mdDoc "Hostname to serve mainsail on";
+    };
+
+    nginx = mkOption {
+      type = types.submodule
+        (import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
+      default = { };
+      example = literalExpression ''
+        {
+          serverAliases = [ "mainsail.''${config.networking.domain}" ];
+        }
+      '';
+      description = lib.mdDoc "Extra configuration for the nginx virtual host of mainsail.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.nginx = {
+      enable = true;
+      upstreams.mainsail-apiserver.servers."${moonraker.address}:${toString moonraker.port}" = { };
+      virtualHosts."${cfg.hostName}" = mkMerge [
+        cfg.nginx
+        {
+          root = mkForce "${cfg.package}/share/mainsail";
+          locations = {
+            "/" = {
+              index = "index.html";
+              tryFiles = "$uri $uri/ /index.html";
+            };
+            "/index.html".extraConfig = ''
+              add_header Cache-Control "no-store, no-cache, must-revalidate";
+            '';
+            "/websocket" = {
+              proxyWebsockets = true;
+              proxyPass = "http://mainsail-apiserver/websocket";
+            };
+            "~ ^/(printer|api|access|machine|server)/" = {
+              proxyWebsockets = true;
+              proxyPass = "http://mainsail-apiserver$request_uri";
+            };
+          };
+        }
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix
index b6e2309555f..2aab97438b7 100644
--- a/nixos/modules/services/web-apps/mastodon.nix
+++ b/nixos/modules/services/web-apps/mastodon.nix
@@ -48,6 +48,8 @@ let
     # User and group
     User = cfg.user;
     Group = cfg.group;
+    # Working directory
+    WorkingDirectory = cfg.package;
     # State directory and mode
     StateDirectory = "mastodon";
     StateDirectoryMode = "0750";
@@ -89,17 +91,17 @@ let
 
   envFile = pkgs.writeText "mastodon.env" (lib.concatMapStrings (s: s + "\n") (
     (lib.concatLists (lib.mapAttrsToList (name: value:
-      if value != null then [
-        "${name}=\"${toString value}\""
-      ] else []
+      lib.optional (value != null) ''${name}="${toString value}"''
     ) env))));
 
-  mastodonTootctl = pkgs.writeShellScriptBin "mastodon-tootctl" ''
-    #! ${pkgs.runtimeShell}
+  mastodonTootctl = let
+    sourceExtraEnv = lib.concatMapStrings (p: "source ${p}\n") cfg.extraEnvFiles;
+  in pkgs.writeShellScriptBin "mastodon-tootctl" ''
     set -a
     export RAILS_ROOT="${cfg.package}"
     source "${envFile}"
     source /var/lib/mastodon/.secrets_env
+    ${sourceExtraEnv}
 
     sudo=exec
     if [[ "$USER" != ${cfg.user} ]]; then
@@ -108,6 +110,37 @@ let
     $sudo ${cfg.package}/bin/tootctl "$@"
   '';
 
+  sidekiqUnits = lib.attrsets.mapAttrs' (name: processCfg:
+    lib.nameValuePair "mastodon-sidekiq-${name}" (let
+      jobClassArgs = toString (builtins.map (c: "-q ${c}") processCfg.jobClasses);
+      jobClassLabel = toString ([""] ++ processCfg.jobClasses);
+      threads = toString (if processCfg.threads == null then cfg.sidekiqThreads else processCfg.threads);
+    in {
+      after = [ "network.target" "mastodon-init-dirs.service" ]
+        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
+        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
+      requires = [ "mastodon-init-dirs.service" ]
+        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
+        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
+      description = "Mastodon sidekiq${jobClassLabel}";
+      wantedBy = [ "mastodon.target" ];
+      environment = env // {
+        PORT = toString(cfg.sidekiqPort);
+        DB_POOL = threads;
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/sidekiq ${jobClassArgs} -c ${threads} -r ${cfg.package}";
+        Restart = "always";
+        RestartSec = 20;
+        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
+        WorkingDirectory = cfg.package;
+        # System Call Filtering
+        SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
+      } // cfgService;
+      path = with pkgs; [ file imagemagick ffmpeg ];
+    })
+  ) cfg.sidekiqProcesses;
+
 in {
 
   options = {
@@ -193,12 +226,53 @@ in {
         type = lib.types.port;
         default = 55002;
       };
+
       sidekiqThreads = lib.mkOption {
-        description = lib.mdDoc "Worker threads used by the mastodon-sidekiq service.";
+        description = lib.mdDoc "Worker threads used by the mastodon-sidekiq-all service. If `sidekiqProcesses` is configured and any processes specify null `threads`, this value is used.";
         type = lib.types.int;
         default = 25;
       };
 
+      sidekiqProcesses = lib.mkOption {
+        description = lib.mdDoc "How many Sidekiq processes should be used to handle background jobs, and which job classes they handle. *Read the [upstream documentation](https://docs.joinmastodon.org/admin/scaling/#sidekiq) before configuring this!*";
+        type = with lib.types; attrsOf (submodule {
+          options = {
+            jobClasses = lib.mkOption {
+              type = listOf (enum [ "default" "push" "pull" "mailers" "scheduler" "ingress" ]);
+              description = lib.mdDoc "If not empty, which job classes should be executed by this process. *Only one process should handle the 'scheduler' class. If left empty, this process will handle the 'scheduler' class.*";
+            };
+            threads = lib.mkOption {
+              type = nullOr int;
+              description = lib.mdDoc "Number of threads this process should use for executing jobs. If null, the configured `sidekiqThreads` are used.";
+            };
+          };
+        });
+        default = {
+          all = {
+            jobClasses = [ ];
+            threads = null;
+          };
+        };
+        example = {
+          all = {
+            jobClasses = [ ];
+            threads = null;
+          };
+          ingress = {
+            jobClasses = [ "ingress" ];
+            threads = 5;
+          };
+          default = {
+            jobClasses = [ "default" ];
+            threads = 10;
+          };
+          push-pull = {
+            jobClasses = [ "push" "pull" ];
+            threads = 5;
+          };
+        };
+      };
+
       vapidPublicKeyFile = lib.mkOption {
         description = lib.mdDoc ''
           Path to file containing the public key used for Web Push
@@ -428,6 +502,15 @@ in {
         '';
       };
 
+      extraEnvFiles = lib.mkOption {
+        type = with lib.types; listOf path;
+        default = [];
+        description = lib.mdDoc ''
+          Extra environment files to pass to all mastodon services. Useful for passing down environmental secrets.
+        '';
+        example = [ "/etc/mastodon/s3config.env" ];
+      };
+
       automaticMigrations = lib.mkOption {
         type = lib.types.bool;
         default = true;
@@ -471,7 +554,7 @@ in {
     };
   };
 
-  config = lib.mkIf cfg.enable {
+  config = lib.mkIf cfg.enable (lib.mkMerge [{
     assertions = [
       {
         assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user);
@@ -502,10 +585,24 @@ in {
             <option>services.mastodon.smtp.authenticate</option> is enabled.
         '';
       }
+      {
+        assertion = 1 ==
+          (lib.count (x: x)
+            (lib.mapAttrsToList
+              (_: v: builtins.elem "scheduler" v.jobClasses || v.jobClasses == [ ])
+              cfg.sidekiqProcesses));
+        message = "There must be exactly one Sidekiq queue in services.mastodon.sidekiqProcesses with jobClass \"scheduler\".";
+      }
     ];
 
     environment.systemPackages = [ mastodonTootctl ];
 
+    systemd.targets.mastodon = {
+      description = "Target for all Mastodon services";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+    };
+
     systemd.services.mastodon-init-dirs = {
       script = ''
         umask 077
@@ -540,7 +637,7 @@ in {
       environment = env;
       serviceConfig = {
         Type = "oneshot";
-        WorkingDirectory = cfg.package;
+        SyslogIdentifier = "mastodon-init-dirs";
         # System Call Filtering
         SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
       } // cfgService;
@@ -580,7 +677,7 @@ in {
       };
       serviceConfig = {
         Type = "oneshot";
-        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ];
+        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
         WorkingDirectory = cfg.package;
         # System Call Filtering
         SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
@@ -598,7 +695,7 @@ in {
       requires = [ "mastodon-init-dirs.service" ]
         ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
         ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
-      wantedBy = [ "multi-user.target" ];
+      wantedBy = [ "mastodon.target" ];
       description = "Mastodon streaming";
       environment = env // (if cfg.enableUnixSocket
         then { SOCKET = "/run/mastodon-streaming/streaming.socket"; }
@@ -608,7 +705,7 @@ in {
         ExecStart = "${cfg.package}/run-streaming.sh";
         Restart = "always";
         RestartSec = 20;
-        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ];
+        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
         WorkingDirectory = cfg.package;
         # Runtime directory and mode
         RuntimeDirectory = "mastodon-streaming";
@@ -625,7 +722,7 @@ in {
       requires = [ "mastodon-init-dirs.service" ]
         ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
         ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
-      wantedBy = [ "multi-user.target" ];
+      wantedBy = [ "mastodon.target" ];
       description = "Mastodon web";
       environment = env // (if cfg.enableUnixSocket
         then { SOCKET = "/run/mastodon-web/web.socket"; }
@@ -635,7 +732,7 @@ in {
         ExecStart = "${cfg.package}/bin/puma -C config/puma.rb";
         Restart = "always";
         RestartSec = 20;
-        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ];
+        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
         WorkingDirectory = cfg.package;
         # Runtime directory and mode
         RuntimeDirectory = "mastodon-web";
@@ -646,37 +743,12 @@ in {
       path = with pkgs; [ file imagemagick ffmpeg ];
     };
 
-    systemd.services.mastodon-sidekiq = {
-      after = [ "network.target" "mastodon-init-dirs.service" ]
-        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
-        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
-      requires = [ "mastodon-init-dirs.service" ]
-        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
-        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
-      wantedBy = [ "multi-user.target" ];
-      description = "Mastodon sidekiq";
-      environment = env // {
-        PORT = toString(cfg.sidekiqPort);
-        DB_POOL = toString cfg.sidekiqThreads;
-      };
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/sidekiq -c ${toString cfg.sidekiqThreads} -r ${cfg.package}";
-        Restart = "always";
-        RestartSec = 20;
-        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ];
-        WorkingDirectory = cfg.package;
-        # System Call Filtering
-        SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
-      } // cfgService;
-      path = with pkgs; [ file imagemagick ffmpeg ];
-    };
-
     systemd.services.mastodon-media-auto-remove = lib.mkIf cfg.mediaAutoRemove.enable {
       description = "Mastodon media auto remove";
       environment = env;
       serviceConfig = {
         Type = "oneshot";
-        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ];
+        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
       } // cfgService;
       script = let
         olderThanDays = toString cfg.mediaAutoRemove.olderThanDays;
@@ -746,7 +818,9 @@ in {
     ];
 
     users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user;
-  };
+  }
+  { systemd.services = sidekiqUnits; }
+  ]);
 
   meta.maintainers = with lib.maintainers; [ happy-river erictapen ];
 
diff --git a/nixos/modules/services/web-apps/matomo-doc.xml b/nixos/modules/services/web-apps/matomo-doc.xml
deleted file mode 100644
index 69d1170e452..00000000000
--- a/nixos/modules/services/web-apps/matomo-doc.xml
+++ /dev/null
@@ -1,107 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-matomo">
- <title>Matomo</title>
- <para>
-  Matomo is a real-time web analytics application. This module configures
-  php-fpm as backend for Matomo, optionally configuring an nginx vhost as well.
- </para>
- <para>
-  An automatic setup is not suported by Matomo, so you need to configure Matomo
-  itself in the browser-based Matomo setup.
- </para>
- <section xml:id="module-services-matomo-database-setup">
-  <title>Database Setup</title>
-
-  <para>
-   You also need to configure a MariaDB or MySQL database and -user for Matomo
-   yourself, and enter those credentials in your browser. You can use
-   passwordless database authentication via the UNIX_SOCKET authentication
-   plugin with the following SQL commands:
-<programlisting>
-# For MariaDB
-INSTALL PLUGIN unix_socket SONAME 'auth_socket';
-CREATE DATABASE matomo;
-CREATE USER 'matomo'@'localhost' IDENTIFIED WITH unix_socket;
-GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
-
-# For MySQL
-INSTALL PLUGIN auth_socket SONAME 'auth_socket.so';
-CREATE DATABASE matomo;
-CREATE USER 'matomo'@'localhost' IDENTIFIED WITH auth_socket;
-GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
-</programlisting>
-   Then fill in <literal>matomo</literal> as database user and database name,
-   and leave the password field blank. This authentication works by allowing
-   only the <literal>matomo</literal> unix user to authenticate as the
-   <literal>matomo</literal> database user (without needing a password), but no
-   other users. For more information on passwordless login, see
-   <link xlink:href="https://mariadb.com/kb/en/mariadb/unix_socket-authentication-plugin/" />.
-  </para>
-
-  <para>
-   Of course, you can use password based authentication as well, e.g. when the
-   database is not on the same host.
-  </para>
- </section>
- <section xml:id="module-services-matomo-archive-processing">
-  <title>Archive Processing</title>
-
-  <para>
-   This module comes with the systemd service
-   <literal>matomo-archive-processing.service</literal> and a timer that
-   automatically triggers archive processing every hour. This means that you
-   can safely
-   <link xlink:href="https://matomo.org/docs/setup-auto-archiving/#disable-browser-triggers-for-matomo-archiving-and-limit-matomo-reports-to-updating-every-hour">
-   disable browser triggers for Matomo archiving </link> at
-   <literal>Administration > System > General Settings</literal>.
-  </para>
-
-  <para>
-   With automatic archive processing, you can now also enable to
-   <link xlink:href="https://matomo.org/docs/privacy/#step-2-delete-old-visitors-logs">
-   delete old visitor logs </link> at <literal>Administration > System >
-   Privacy</literal>, but make sure that you run <literal>systemctl start
-   matomo-archive-processing.service</literal> at least once without errors if
-   you have already collected data before, so that the reports get archived
-   before the source data gets deleted.
-  </para>
- </section>
- <section xml:id="module-services-matomo-backups">
-  <title>Backup</title>
-
-  <para>
-   You only need to take backups of your MySQL database and the
-   <filename>/var/lib/matomo/config/config.ini.php</filename> file. Use a user
-   in the <literal>matomo</literal> group or root to access the file. For more
-   information, see
-   <link xlink:href="https://matomo.org/faq/how-to-install/faq_138/" />.
-  </para>
- </section>
- <section xml:id="module-services-matomo-issues">
-  <title>Issues</title>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Matomo will warn you that the JavaScript tracker is not writable. This is
-     because it's located in the read-only nix store. You can safely ignore
-     this, unless you need a plugin that needs JavaScript tracker access.
-    </para>
-   </listitem>
-  </itemizedlist>
- </section>
- <section xml:id="module-services-matomo-other-web-servers">
-  <title>Using other Web Servers than nginx</title>
-
-  <para>
-   You can use other web servers by forwarding calls for
-   <filename>index.php</filename> and <filename>piwik.php</filename> to the
-   <literal><link linkend="opt-services.phpfpm.pools._name_.socket">services.phpfpm.pools.&lt;name&gt;.socket</link></literal> fastcgi unix socket. You can use
-   the nginx configuration in the module code as a reference to what else
-   should be configured.
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/matomo.md b/nixos/modules/services/web-apps/matomo.md
new file mode 100644
index 00000000000..e750c0c1477
--- /dev/null
+++ b/nixos/modules/services/web-apps/matomo.md
@@ -0,0 +1,77 @@
+# Matomo {#module-services-matomo}
+
+Matomo is a real-time web analytics application. This module configures
+php-fpm as backend for Matomo, optionally configuring an nginx vhost as well.
+
+An automatic setup is not supported by Matomo, so you need to configure Matomo
+itself in the browser-based Matomo setup.
+
+## Database Setup {#module-services-matomo-database-setup}
+
+You also need to configure a MariaDB or MySQL database and -user for Matomo
+yourself, and enter those credentials in your browser. You can use
+passwordless database authentication via the UNIX_SOCKET authentication
+plugin with the following SQL commands:
+```
+# For MariaDB
+INSTALL PLUGIN unix_socket SONAME 'auth_socket';
+CREATE DATABASE matomo;
+CREATE USER 'matomo'@'localhost' IDENTIFIED WITH unix_socket;
+GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
+
+# For MySQL
+INSTALL PLUGIN auth_socket SONAME 'auth_socket.so';
+CREATE DATABASE matomo;
+CREATE USER 'matomo'@'localhost' IDENTIFIED WITH auth_socket;
+GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
+```
+Then fill in `matomo` as database user and database name,
+and leave the password field blank. This authentication works by allowing
+only the `matomo` unix user to authenticate as the
+`matomo` database user (without needing a password), but no
+other users. For more information on passwordless login, see
+<https://mariadb.com/kb/en/mariadb/unix_socket-authentication-plugin/>.
+
+Of course, you can use password based authentication as well, e.g. when the
+database is not on the same host.
+
+## Archive Processing {#module-services-matomo-archive-processing}
+
+This module comes with the systemd service
+`matomo-archive-processing.service` and a timer that
+automatically triggers archive processing every hour. This means that you
+can safely
+[disable browser triggers for Matomo archiving](
+https://matomo.org/docs/setup-auto-archiving/#disable-browser-triggers-for-matomo-archiving-and-limit-matomo-reports-to-updating-every-hour
+) at
+`Administration > System > General Settings`.
+
+With automatic archive processing, you can now also enable to
+[delete old visitor logs](https://matomo.org/docs/privacy/#step-2-delete-old-visitors-logs)
+at `Administration > System > Privacy`, but make sure that you run `systemctl start
+matomo-archive-processing.service` at least once without errors if
+you have already collected data before, so that the reports get archived
+before the source data gets deleted.
+
+## Backup {#module-services-matomo-backups}
+
+You only need to take backups of your MySQL database and the
+{file}`/var/lib/matomo/config/config.ini.php` file. Use a user
+in the `matomo` group or root to access the file. For more
+information, see
+<https://matomo.org/faq/how-to-install/faq_138/>.
+
+## Issues {#module-services-matomo-issues}
+
+  - Matomo will warn you that the JavaScript tracker is not writable. This is
+    because it's located in the read-only nix store. You can safely ignore
+    this, unless you need a plugin that needs JavaScript tracker access.
+
+## Using other Web Servers than nginx {#module-services-matomo-other-web-servers}
+
+You can use other web servers by forwarding calls for
+{file}`index.php` and {file}`piwik.php` to the
+[`services.phpfpm.pools.<name>.socket`](#opt-services.phpfpm.pools._name_.socket)
+fastcgi unix socket. You can use
+the nginx configuration in the module code as a reference to what else
+should be configured.
diff --git a/nixos/modules/services/web-apps/matomo.nix b/nixos/modules/services/web-apps/matomo.nix
index 0435d21ce8a..eadf8b62b97 100644
--- a/nixos/modules/services/web-apps/matomo.nix
+++ b/nixos/modules/services/web-apps/matomo.nix
@@ -325,7 +325,7 @@ in {
   };
 
   meta = {
-    doc = ./matomo-doc.xml;
+    doc = ./matomo.md;
     maintainers = with lib.maintainers; [ florianjacob ];
   };
 }
diff --git a/nixos/modules/services/web-apps/mattermost.nix b/nixos/modules/services/web-apps/mattermost.nix
index 56a53198b3f..66e5f1695a1 100644
--- a/nixos/modules/services/web-apps/mattermost.nix
+++ b/nixos/modules/services/web-apps/mattermost.nix
@@ -86,8 +86,7 @@ let
   mattermostConf = recursiveUpdate
     mattermostConfWithoutPlugins
     (
-      if mattermostPlugins == null then {}
-      else {
+      lib.optionalAttrs (mattermostPlugins != null) {
         PluginSettings = {
           Enable = true;
         };
@@ -184,6 +183,22 @@ in
           .tar.gz files.
         '';
       };
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = lib.mdDoc ''
+          Environment file (see {manpage}`systemd.exec(5)`
+          "EnvironmentFile=" section for the syntax) which sets config options
+          for mattermost (see [the mattermost documentation](https://docs.mattermost.com/configure/configuration-settings.html#environment-variables)).
+
+          Settings defined in the environment file will overwrite settings
+          set via nix or via the {option}`services.mattermost.extraConfig`
+          option.
+
+          Useful for setting config options without their value ending up in the
+          (world-readable) nix store, e.g. for a database password.
+        '';
+      };
 
       localDatabaseCreate = mkOption {
         type = types.bool;
@@ -321,6 +336,7 @@ in
           Restart = "always";
           RestartSec = "10";
           LimitNOFILE = "49152";
+          EnvironmentFile = cfg.environmentFile;
         };
         unitConfig.JoinsNamespaceOf = mkIf cfg.localDatabaseCreate "postgresql.service";
       };
diff --git a/nixos/modules/services/web-apps/mediawiki.nix b/nixos/modules/services/web-apps/mediawiki.nix
index 07f29674862..21c587694c6 100644
--- a/nixos/modules/services/web-apps/mediawiki.nix
+++ b/nixos/modules/services/web-apps/mediawiki.nix
@@ -8,7 +8,8 @@ let
   cfg = config.services.mediawiki;
   fpm = config.services.phpfpm.pools.mediawiki;
   user = "mediawiki";
-  group = config.services.httpd.group;
+  group = if cfg.webserver == "apache" then config.services.httpd.group else "mediawiki";
+
   cacheDir = "/var/cache/mediawiki";
   stateDir = "/var/lib/mediawiki";
 
@@ -46,6 +47,15 @@ let
     done
   '';
 
+  dbAddr = if cfg.database.socket == null then
+    "${cfg.database.host}:${toString cfg.database.port}"
+  else if cfg.database.type == "mysql" then
+    "${cfg.database.host}:${cfg.database.socket}"
+  else if cfg.database.type == "postgres" then
+    "${cfg.database.socket}"
+  else
+    throw "Unsupported database type: ${cfg.database.type} for socket: ${cfg.database.socket}";
+
   mediawikiConfig = pkgs.writeText "LocalSettings.php" ''
     <?php
       # Protect against web entry
@@ -64,7 +74,7 @@ let
       $wgScriptPath = "";
 
       ## The protocol and server name to use in fully-qualified URLs
-      $wgServer = "${if cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL then "https" else "http"}://${cfg.virtualHost.hostName}";
+      $wgServer = "${cfg.url}";
 
       ## The URL path to static resources (images, scripts, etc.)
       $wgResourceBasePath = $wgScriptPath;
@@ -78,8 +88,7 @@ let
       $wgEnableEmail = true;
       $wgEnableUserEmail = true; # UPO
 
-      $wgEmergencyContact = "${if cfg.virtualHost.adminAddr != null then cfg.virtualHost.adminAddr else config.services.httpd.adminAddr}";
-      $wgPasswordSender = $wgEmergencyContact;
+      $wgPasswordSender = "${cfg.passwordSender}";
 
       $wgEnotifUserTalk = false; # UPO
       $wgEnotifWatchlist = false; # UPO
@@ -87,7 +96,8 @@ let
 
       ## Database settings
       $wgDBtype = "${cfg.database.type}";
-      $wgDBserver = "${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}";
+      $wgDBserver = "${dbAddr}";
+      $wgDBport = "${toString cfg.database.port}";
       $wgDBname = "${cfg.database.name}";
       $wgDBuser = "${cfg.database.user}";
       ${optionalString (cfg.database.passwordFile != null) "$wgDBpassword = file_get_contents(\"${cfg.database.passwordFile}\");"}
@@ -180,6 +190,16 @@ in
         description = lib.mdDoc "Which MediaWiki package to use.";
       };
 
+      finalPackage = mkOption {
+        type = types.package;
+        readOnly = true;
+        default = pkg;
+        defaultText = literalExpression "pkg";
+        description = lib.mdDoc ''
+          The final package used by the module. This is the package that will have extensions and skins installed.
+        '';
+      };
+
       name = mkOption {
         type = types.str;
         default = "MediaWiki";
@@ -187,6 +207,22 @@ in
         description = lib.mdDoc "Name of the wiki.";
       };
 
+      url = mkOption {
+        type = types.str;
+        default = if cfg.webserver == "apache" then
+            "${if cfg.httpd.virtualHost.addSSL || cfg.httpd.virtualHost.forceSSL || cfg.httpd.virtualHost.onlySSL then "https" else "http"}://${cfg.httpd.virtualHost.hostName}"
+          else
+            "http://localhost";
+        defaultText = literalExpression ''
+          if cfg.webserver == "apache" then
+            "''${if cfg.httpd.virtualHost.addSSL || cfg.httpd.virtualHost.forceSSL || cfg.httpd.virtualHost.onlySSL then "https" else "http"}://''${cfg.httpd.virtualHost.hostName}"
+          else
+            "http://localhost";
+        '';
+        example = "https://wiki.example.org";
+        description = lib.mdDoc "URL of the wiki.";
+      };
+
       uploadsDir = mkOption {
         type = types.nullOr types.path;
         default = "${stateDir}/uploads";
@@ -202,6 +238,24 @@ in
         example = "/run/keys/mediawiki-password";
       };
 
+      passwordSender = mkOption {
+        type = types.str;
+        default =
+          if cfg.webserver == "apache" then
+            if cfg.httpd.virtualHost.adminAddr != null then
+              cfg.httpd.virtualHost.adminAddr
+            else
+              config.services.httpd.adminAddr else "root@localhost";
+        defaultText = literalExpression ''
+          if cfg.webserver == "apache" then
+            if cfg.httpd.virtualHost.adminAddr != null then
+              cfg.httpd.virtualHost.adminAddr
+            else
+              config.services.httpd.adminAddr else "root@localhost"
+        '';
+        description = lib.mdDoc "Contact address for password reset.";
+      };
+
       skins = mkOption {
         default = {};
         type = types.attrsOf types.path;
@@ -231,6 +285,12 @@ in
         '';
       };
 
+      webserver = mkOption {
+        type = types.enum [ "apache" "none" ];
+        default = "apache";
+        description = lib.mdDoc "Webserver to use.";
+      };
+
       database = {
         type = mkOption {
           type = types.enum [ "mysql" "postgres" "sqlite" "mssql" "oracle" ];
@@ -246,7 +306,8 @@ in
 
         port = mkOption {
           type = types.port;
-          default = 3306;
+          default = if cfg.database.type == "mysql" then 3306 else 5432;
+          defaultText = literalExpression "3306";
           description = lib.mdDoc "Database host port.";
         };
 
@@ -286,14 +347,19 @@ in
 
         socket = mkOption {
           type = types.nullOr types.path;
-          default = if cfg.database.createLocally then "/run/mysqld/mysqld.sock" else null;
+          default = if (cfg.database.type == "mysql" && cfg.database.createLocally) then
+              "/run/mysqld/mysqld.sock"
+            else if (cfg.database.type == "postgres" && cfg.database.createLocally) then
+              "/run/postgresql"
+            else
+              null;
           defaultText = literalExpression "/run/mysqld/mysqld.sock";
           description = lib.mdDoc "Path to the unix socket file to use for authentication.";
         };
 
         createLocally = mkOption {
           type = types.bool;
-          default = cfg.database.type == "mysql";
+          default = cfg.database.type == "mysql" || cfg.database.type == "postgres";
           defaultText = literalExpression "true";
           description = lib.mdDoc ''
             Create the database and database user locally.
@@ -302,7 +368,7 @@ in
         };
       };
 
-      virtualHost = mkOption {
+      httpd.virtualHost = mkOption {
         type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
         example = literalExpression ''
           {
@@ -350,12 +416,16 @@ in
     };
   };
 
+  imports = [
+    (lib.mkRenamedOptionModule [ "services" "mediawiki" "virtualHost" ] [ "services" "mediawiki" "httpd" "virtualHost" ])
+  ];
+
   # implementation
   config = mkIf cfg.enable {
 
     assertions = [
-      { assertion = cfg.database.createLocally -> cfg.database.type == "mysql";
-        message = "services.mediawiki.createLocally is currently only supported for database type 'mysql'";
+      { assertion = cfg.database.createLocally -> (cfg.database.type == "mysql" || cfg.database.type == "postgres");
+        message = "services.mediawiki.createLocally is currently only supported for database type 'mysql' and 'postgres'";
       }
       { assertion = cfg.database.createLocally -> cfg.database.user == user;
         message = "services.mediawiki.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true";
@@ -374,50 +444,64 @@ in
       Vector = "${cfg.package}/share/mediawiki/skins/Vector";
     };
 
-    services.mysql = mkIf cfg.database.createLocally {
+    services.mysql = mkIf (cfg.database.type == "mysql" && cfg.database.createLocally) {
       enable = true;
       package = mkDefault pkgs.mariadb;
       ensureDatabases = [ cfg.database.name ];
-      ensureUsers = [
-        { name = cfg.database.user;
-          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
-        }
-      ];
+      ensureUsers = [{
+        name = cfg.database.user;
+        ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+      }];
+    };
+
+    services.postgresql = mkIf (cfg.database.type == "postgres" && cfg.database.createLocally) {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [{
+        name = cfg.database.user;
+        ensurePermissions = { "DATABASE \"${cfg.database.name}\"" = "ALL PRIVILEGES"; };
+      }];
     };
 
     services.phpfpm.pools.mediawiki = {
       inherit user group;
       phpEnv.MEDIAWIKI_CONFIG = "${mediawikiConfig}";
-      settings = {
+      settings = (if (cfg.webserver == "apache") then {
         "listen.owner" = config.services.httpd.user;
         "listen.group" = config.services.httpd.group;
-      } // cfg.poolConfig;
+      } else {
+        "listen.owner" = user;
+        "listen.group" = group;
+      }) // cfg.poolConfig;
     };
 
-    services.httpd = {
+    services.httpd = lib.mkIf (cfg.webserver == "apache") {
       enable = true;
       extraModules = [ "proxy_fcgi" ];
-      virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
-        documentRoot = mkForce "${pkg}/share/mediawiki";
-        extraConfig = ''
-          <Directory "${pkg}/share/mediawiki">
-            <FilesMatch "\.php$">
-              <If "-f %{REQUEST_FILENAME}">
-                SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
-              </If>
-            </FilesMatch>
-
-            Require all granted
-            DirectoryIndex index.php
-            AllowOverride All
-          </Directory>
-        '' + optionalString (cfg.uploadsDir != null) ''
-          Alias "/images" "${cfg.uploadsDir}"
-          <Directory "${cfg.uploadsDir}">
-            Require all granted
-          </Directory>
-        '';
-      } ];
+      virtualHosts.${cfg.httpd.virtualHost.hostName} = mkMerge [
+        cfg.httpd.virtualHost
+        {
+          documentRoot = mkForce "${pkg}/share/mediawiki";
+          extraConfig = ''
+            <Directory "${pkg}/share/mediawiki">
+              <FilesMatch "\.php$">
+                <If "-f %{REQUEST_FILENAME}">
+                  SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
+                </If>
+              </FilesMatch>
+
+              Require all granted
+              DirectoryIndex index.php
+              AllowOverride All
+            </Directory>
+          '' + optionalString (cfg.uploadsDir != null) ''
+            Alias "/images" "${cfg.uploadsDir}"
+            <Directory "${cfg.uploadsDir}">
+              Require all granted
+            </Directory>
+          '';
+        }
+      ];
     };
 
     systemd.tmpfiles.rules = [
@@ -431,7 +515,8 @@ in
     systemd.services.mediawiki-init = {
       wantedBy = [ "multi-user.target" ];
       before = [ "phpfpm-mediawiki.service" ];
-      after = optional cfg.database.createLocally "mysql.service";
+      after = optional (cfg.database.type == "mysql" && cfg.database.createLocally) "mysql.service"
+              ++ optional (cfg.database.type == "postgres" && cfg.database.createLocally) "postgresql.service";
       script = ''
         if ! test -e "${stateDir}/secret.key"; then
           tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c 64 > ${stateDir}/secret.key
@@ -442,7 +527,7 @@ in
         ${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/install.php \
           --confpath /tmp \
           --scriptpath / \
-          --dbserver ${cfg.database.host}${optionalString (cfg.database.socket != null) ":${cfg.database.socket}"} \
+          --dbserver "${dbAddr}" \
           --dbport ${toString cfg.database.port} \
           --dbname ${cfg.database.name} \
           ${optionalString (cfg.database.tablePrefix != null) "--dbprefix ${cfg.database.tablePrefix}"} \
@@ -464,12 +549,14 @@ in
       };
     };
 
-    systemd.services.httpd.after = optional (cfg.database.createLocally && cfg.database.type == "mysql") "mysql.service";
+    systemd.services.httpd.after = optional (cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "mysql") "mysql.service"
+      ++ optional (cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "postgres") "postgresql.service";
 
     users.users.${user} = {
       group = group;
       isSystemUser = true;
     };
+    users.groups.${group} = {};
 
     environment.systemPackages = [ mediawikiScripts ];
   };
diff --git a/nixos/modules/services/web-apps/monica.nix b/nixos/modules/services/web-apps/monica.nix
new file mode 100644
index 00000000000..2bff42f7ffa
--- /dev/null
+++ b/nixos/modules/services/web-apps/monica.nix
@@ -0,0 +1,468 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+with lib; let
+  cfg = config.services.monica;
+  monica = pkgs.monica.override {
+    dataDir = cfg.dataDir;
+  };
+  db = cfg.database;
+  mail = cfg.mail;
+
+  user = cfg.user;
+  group = cfg.group;
+
+  # shell script for local administration
+  artisan = pkgs.writeScriptBin "monica" ''
+    #! ${pkgs.runtimeShell}
+    cd ${monica}
+    sudo() {
+      if [[ "$USER" != ${user} ]]; then
+        exec /run/wrappers/bin/sudo -u ${user} "$@"
+      else
+        exec "$@"
+      fi
+    }
+    sudo ${pkgs.php}/bin/php artisan "$@"
+  '';
+
+  tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
+in {
+  options.services.monica = {
+    enable = mkEnableOption (lib.mdDoc "monica");
+
+    user = mkOption {
+      default = "monica";
+      description = lib.mdDoc "User monica runs as.";
+      type = types.str;
+    };
+
+    group = mkOption {
+      default = "monica";
+      description = lib.mdDoc "Group monica runs as.";
+      type = types.str;
+    };
+
+    appKeyFile = mkOption {
+      description = lib.mdDoc ''
+        A file containing the Laravel APP_KEY - a 32 character long,
+        base64 encoded key used for encryption where needed. Can be
+        generated with <code>head -c 32 /dev/urandom | base64</code>.
+      '';
+      example = "/run/keys/monica-appkey";
+      type = types.path;
+    };
+
+    hostname = lib.mkOption {
+      type = lib.types.str;
+      default =
+        if config.networking.domain != null
+        then config.networking.fqdn
+        else config.networking.hostName;
+      defaultText = lib.literalExpression "config.networking.fqdn";
+      example = "monica.example.com";
+      description = lib.mdDoc ''
+        The hostname to serve monica on.
+      '';
+    };
+
+    appURL = mkOption {
+      description = lib.mdDoc ''
+        The root URL that you want to host monica on. All URLs in monica will be generated using this value.
+        If you change this in the future you may need to run a command to update stored URLs in the database.
+        Command example: <code>php artisan monica:update-url https://old.example.com https://new.example.com</code>
+      '';
+      default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}";
+      defaultText = ''http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostname}'';
+      example = "https://example.com";
+      type = types.str;
+    };
+
+    dataDir = mkOption {
+      description = lib.mdDoc "monica data directory";
+      default = "/var/lib/monica";
+      type = types.path;
+    };
+
+    database = {
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = lib.mdDoc "Database host address.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 3306;
+        description = lib.mdDoc "Database host port.";
+      };
+      name = mkOption {
+        type = types.str;
+        default = "monica";
+        description = lib.mdDoc "Database name.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = user;
+        defaultText = lib.literalExpression "user";
+        description = lib.mdDoc "Database username.";
+      };
+      passwordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/run/keys/monica-dbpassword";
+        description = lib.mdDoc ''
+          A file containing the password corresponding to
+          <option>database.user</option>.
+        '';
+      };
+      createLocally = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Create the database and database user locally.";
+      };
+    };
+
+    mail = {
+      driver = mkOption {
+        type = types.enum ["smtp" "sendmail"];
+        default = "smtp";
+        description = lib.mdDoc "Mail driver to use.";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = lib.mdDoc "Mail host address.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 1025;
+        description = lib.mdDoc "Mail host port.";
+      };
+      fromName = mkOption {
+        type = types.str;
+        default = "monica";
+        description = lib.mdDoc "Mail \"from\" name.";
+      };
+      from = mkOption {
+        type = types.str;
+        default = "mail@monica.com";
+        description = lib.mdDoc "Mail \"from\" email.";
+      };
+      user = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "monica";
+        description = lib.mdDoc "Mail username.";
+      };
+      passwordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/run/keys/monica-mailpassword";
+        description = lib.mdDoc ''
+          A file containing the password corresponding to
+          <option>mail.user</option>.
+        '';
+      };
+      encryption = mkOption {
+        type = with types; nullOr (enum ["tls"]);
+        default = null;
+        description = lib.mdDoc "SMTP encryption mechanism to use.";
+      };
+    };
+
+    maxUploadSize = mkOption {
+      type = types.str;
+      default = "18M";
+      example = "1G";
+      description = lib.mdDoc "The maximum size for uploads (e.g. images).";
+    };
+
+    poolConfig = mkOption {
+      type = with types; attrsOf (oneOf [str int bool]);
+      default = {
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 4;
+        "pm.max_requests" = 500;
+      };
+      description = lib.mdDoc ''
+        Options for the monica PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+        for details on configuration directives.
+      '';
+    };
+
+    nginx = mkOption {
+      type = types.submodule (
+        recursiveUpdate
+        (import ../web-servers/nginx/vhost-options.nix {inherit config lib;}) {}
+      );
+      default = {};
+      example = ''
+        {
+          serverAliases = [
+            "monica.''${config.networking.domain}"
+          ];
+          # To enable encryption and let let's encrypt take care of certificate
+          forceSSL = true;
+          enableACME = true;
+        }
+      '';
+      description = lib.mdDoc ''
+        With this option, you can customize the nginx virtualHost settings.
+      '';
+    };
+
+    config = mkOption {
+      type = with types;
+        attrsOf
+        (nullOr
+          (either
+            (oneOf [
+              bool
+              int
+              port
+              path
+              str
+            ])
+            (submodule {
+              options = {
+                _secret = mkOption {
+                  type = nullOr str;
+                  description = lib.mdDoc ''
+                    The path to a file containing the value the
+                    option should be set to in the final
+                    configuration file.
+                  '';
+                };
+              };
+            })));
+      default = {};
+      example = ''
+        {
+          ALLOWED_IFRAME_HOSTS = "https://example.com";
+          WKHTMLTOPDF = "/home/user/bins/wkhtmltopdf";
+          AUTH_METHOD = "oidc";
+          OIDC_NAME = "MyLogin";
+          OIDC_DISPLAY_NAME_CLAIMS = "name";
+          OIDC_CLIENT_ID = "monica";
+          OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
+          OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
+          OIDC_ISSUER_DISCOVER = true;
+        }
+      '';
+      description = lib.mdDoc ''
+        monica configuration options to set in the
+        <filename>.env</filename> file.
+
+        Refer to <link xlink:href="https://github.com/monicahq/monica"/>
+        for details on supported values.
+
+        Settings containing secret data should be set to an attribute
+        set containing the attribute <literal>_secret</literal> - a
+        string pointing to a file containing the value the option
+        should be set to. See the example to get a better picture of
+        this: in the resulting <filename>.env</filename> file, the
+        <literal>OIDC_CLIENT_SECRET</literal> key will be set to the
+        contents of the <filename>/run/keys/oidc_secret</filename>
+        file.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = db.createLocally -> db.user == user;
+        message = "services.monica.database.user must be set to ${user} if services.monica.database.createLocally is set true.";
+      }
+      {
+        assertion = db.createLocally -> db.passwordFile == null;
+        message = "services.monica.database.passwordFile cannot be specified if services.monica.database.createLocally is set to true.";
+      }
+    ];
+
+    services.monica.config = {
+      APP_ENV = "production";
+      APP_KEY._secret = cfg.appKeyFile;
+      APP_URL = cfg.appURL;
+      DB_HOST = db.host;
+      DB_PORT = db.port;
+      DB_DATABASE = db.name;
+      DB_USERNAME = db.user;
+      MAIL_DRIVER = mail.driver;
+      MAIL_FROM_NAME = mail.fromName;
+      MAIL_FROM = mail.from;
+      MAIL_HOST = mail.host;
+      MAIL_PORT = mail.port;
+      MAIL_USERNAME = mail.user;
+      MAIL_ENCRYPTION = mail.encryption;
+      DB_PASSWORD._secret = db.passwordFile;
+      MAIL_PASSWORD._secret = mail.passwordFile;
+      APP_SERVICES_CACHE = "/run/monica/cache/services.php";
+      APP_PACKAGES_CACHE = "/run/monica/cache/packages.php";
+      APP_CONFIG_CACHE = "/run/monica/cache/config.php";
+      APP_ROUTES_CACHE = "/run/monica/cache/routes-v7.php";
+      APP_EVENTS_CACHE = "/run/monica/cache/events.php";
+      SESSION_SECURE_COOKIE = tlsEnabled;
+    };
+
+    environment.systemPackages = [artisan];
+
+    services.mysql = mkIf db.createLocally {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [db.name];
+      ensureUsers = [
+        {
+          name = db.user;
+          ensurePermissions = {"${db.name}.*" = "ALL PRIVILEGES";};
+        }
+      ];
+    };
+
+    services.phpfpm.pools.monica = {
+      inherit user group;
+      phpOptions = ''
+        log_errors = on
+        post_max_size = ${cfg.maxUploadSize}
+        upload_max_filesize = ${cfg.maxUploadSize}
+      '';
+      settings = {
+        "listen.mode" = "0660";
+        "listen.owner" = user;
+        "listen.group" = group;
+      } // cfg.poolConfig;
+    };
+
+    services.nginx = {
+      enable = mkDefault true;
+      recommendedTlsSettings = true;
+      recommendedOptimisation = true;
+      recommendedGzipSettings = true;
+      recommendedBrotliSettings = true;
+      recommendedProxySettings = true;
+      virtualHosts.${cfg.hostname} = mkMerge [
+        cfg.nginx
+        {
+          root = mkForce "${monica}/public";
+          locations = {
+            "/" = {
+              index = "index.php";
+              tryFiles = "$uri $uri/ /index.php?$query_string";
+            };
+            "~ \.php$".extraConfig = ''
+              fastcgi_pass unix:${config.services.phpfpm.pools."monica".socket};
+            '';
+            "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
+              extraConfig = "expires 365d;";
+            };
+          };
+        }
+      ];
+    };
+
+    systemd.services.monica-setup = {
+      description = "Preparation tasks for monica";
+      before = ["phpfpm-monica.service"];
+      after = optional db.createLocally "mysql.service";
+      wantedBy = ["multi-user.target"];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        User = user;
+        UMask = 077;
+        WorkingDirectory = "${monica}";
+        RuntimeDirectory = "monica/cache";
+        RuntimeDirectoryMode = 0700;
+      };
+      path = [pkgs.replace-secret];
+      script = let
+        isSecret = v: isAttrs v && v ? _secret && isString v._secret;
+        monicaEnvVars = lib.generators.toKeyValue {
+          mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
+            mkValueString = v:
+              with builtins;
+                if isInt v
+                then toString v
+                else if isString v
+                then v
+                else if true == v
+                then "true"
+                else if false == v
+                then "false"
+                else if isSecret v
+                then hashString "sha256" v._secret
+                else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
+          };
+        };
+        secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
+        mkSecretReplacement = file: ''
+          replace-secret ${escapeShellArgs [(builtins.hashString "sha256" file) file "${cfg.dataDir}/.env"]}
+        '';
+        secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
+        filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{} null])) cfg.config;
+        monicaEnv = pkgs.writeText "monica.env" (monicaEnvVars filteredConfig);
+      in ''
+        # error handling
+        set -euo pipefail
+
+        # create .env file
+        install -T -m 0600 -o ${user} ${monicaEnv} "${cfg.dataDir}/.env"
+        ${secretReplacements}
+        if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then
+          sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env"
+        fi
+
+        # migrate & seed db
+        ${pkgs.php}/bin/php artisan key:generate --force
+        ${pkgs.php}/bin/php artisan setup:production -v --force
+      '';
+    };
+
+    systemd.services.monica-scheduler = {
+      description = "Background tasks for monica";
+      startAt = "minutely";
+      after = ["monica-setup.service"];
+      serviceConfig = {
+        Type = "oneshot";
+        User = user;
+        WorkingDirectory = "${monica}";
+        ExecStart = "${pkgs.php}/bin/php ${monica}/artisan schedule:run -v";
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${cfg.dataDir}                            0710 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public                     0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads             0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage                    0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/app                0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/fonts              0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/framework          0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/framework/cache    0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/framework/views    0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/logs               0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/uploads            0700 ${user} ${group} - -"
+    ];
+
+    users = {
+      users = mkIf (user == "monica") {
+        monica = {
+          inherit group;
+          isSystemUser = true;
+        };
+        "${config.services.nginx.user}".extraGroups = [group];
+      };
+      groups = mkIf (group == "monica") {
+        monica = {};
+      };
+    };
+  };
+}
+
diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix
index 5f8d9c5b15f..b617e9a5937 100644
--- a/nixos/modules/services/web-apps/moodle.nix
+++ b/nixos/modules/services/web-apps/moodle.nix
@@ -56,7 +56,7 @@ let
   mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
 
-  phpExt = pkgs.php80.buildEnv {
+  phpExt = pkgs.php81.buildEnv {
     extensions = { all, ... }: with all; [ iconv mbstring curl openssl tokenizer soap ctype zip gd simplexml dom intl sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo filter opcache exif sodium ];
     extraConfig = "max_input_vars = 5000";
   };
diff --git a/nixos/modules/services/web-apps/netbox.nix b/nixos/modules/services/web-apps/netbox.nix
index e028f16004e..e2ef350ba4e 100644
--- a/nixos/modules/services/web-apps/netbox.nix
+++ b/nixos/modules/services/web-apps/netbox.nix
@@ -1,48 +1,20 @@
-{ config, lib, pkgs, buildEnv, ... }:
+{ config, lib, pkgs, ... }:
 
 with lib;
 
 let
   cfg = config.services.netbox;
+  pythonFmt = pkgs.formats.pythonVars {};
   staticDir = cfg.dataDir + "/static";
-  configFile = pkgs.writeTextFile {
-    name = "configuration.py";
-    text = ''
-      STATIC_ROOT = '${staticDir}'
-      MEDIA_ROOT = '${cfg.dataDir}/media'
-      REPORTS_ROOT = '${cfg.dataDir}/reports'
-      SCRIPTS_ROOT = '${cfg.dataDir}/scripts'
-
-      ALLOWED_HOSTS = ['*']
-      DATABASE = {
-        'NAME': 'netbox',
-        'USER': 'netbox',
-        'HOST': '/run/postgresql',
-      }
-
-      # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
-      # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
-      # to use two separate database IDs.
-      REDIS = {
-          'tasks': {
-              'URL': 'unix://${config.services.redis.servers.netbox.unixSocket}?db=0',
-              'SSL': False,
-          },
-          'caching': {
-              'URL': 'unix://${config.services.redis.servers.netbox.unixSocket}?db=1',
-              'SSL': False,
-          }
-      }
-
-      with open("${cfg.secretKeyFile}", "r") as file:
-          SECRET_KEY = file.readline()
-
-      ${optionalString cfg.enableLdap "REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend'"}
-
-      ${cfg.extraConfig}
-    '';
+
+  settingsFile = pythonFmt.generate "netbox-settings.py" cfg.settings;
+  extraConfigFile = pkgs.writeTextFile {
+    name = "netbox-extraConfig.py";
+    text = cfg.extraConfig;
   };
-  pkg = (pkgs.netbox.overrideAttrs (old: {
+  configFile = pkgs.concatText "configuration.py" [ settingsFile extraConfigFile ];
+
+  pkg = (cfg.package.overrideAttrs (old: {
     installPhase = old.installPhase + ''
       ln -s ${configFile} $out/opt/netbox/netbox/netbox/configuration.py
     '' + optionalString cfg.enableLdap ''
@@ -70,6 +42,30 @@ in {
       '';
     };
 
+    settings = lib.mkOption {
+      description = lib.mdDoc ''
+        Configuration options to set in `configuration.py`.
+        See the [documentation](https://docs.netbox.dev/en/stable/configuration/) for more possible options.
+      '';
+
+      default = { };
+
+      type = lib.types.submodule {
+        freeformType = pythonFmt.type;
+
+        options = {
+          ALLOWED_HOSTS = lib.mkOption {
+            type = with lib.types; listOf str;
+            default = ["*"];
+            description = lib.mdDoc ''
+              A list of valid fully-qualified domain names (FQDNs) and/or IP
+              addresses that can be used to reach the NetBox service.
+            '';
+          };
+        };
+      };
+    };
+
     listenAddress = mkOption {
       type = types.str;
       default = "[::1]";
@@ -78,6 +74,17 @@ in {
       '';
     };
 
+    package = mkOption {
+      type = types.package;
+      default = if versionAtLeast config.system.stateVersion "23.05" then pkgs.netbox else pkgs.netbox_3_3;
+      defaultText = literalExpression ''
+        if versionAtLeast config.system.stateVersion "23.05" then pkgs.netbox else pkgs.netbox_3_3;
+      '';
+      description = lib.mdDoc ''
+        NetBox package to use.
+      '';
+    };
+
     port = mkOption {
       type = types.port;
       default = 8001;
@@ -117,7 +124,7 @@ in {
       default = "";
       description = lib.mdDoc ''
         Additional lines of configuration appended to the `configuration.py`.
-        See the [documentation](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/) for more possible options.
+        See the [documentation](https://docs.netbox.dev/en/stable/configuration/) for more possible options.
       '';
     };
 
@@ -138,11 +145,90 @@ in {
         Path to the Configuration-File for LDAP-Authentication, will be loaded as `ldap_config.py`.
         See the [documentation](https://netbox.readthedocs.io/en/stable/installation/6-ldap/#configuration) for possible options.
       '';
+      example = ''
+        import ldap
+        from django_auth_ldap.config import LDAPSearch, PosixGroupType
+
+        AUTH_LDAP_SERVER_URI = "ldaps://ldap.example.com/"
+
+        AUTH_LDAP_USER_SEARCH = LDAPSearch(
+            "ou=accounts,ou=posix,dc=example,dc=com",
+            ldap.SCOPE_SUBTREE,
+            "(uid=%(user)s)",
+        )
+
+        AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
+            "ou=groups,ou=posix,dc=example,dc=com",
+            ldap.SCOPE_SUBTREE,
+            "(objectClass=posixGroup)",
+        )
+        AUTH_LDAP_GROUP_TYPE = PosixGroupType()
+
+        # Mirror LDAP group assignments.
+        AUTH_LDAP_MIRROR_GROUPS = True
+
+        # For more granular permissions, we can map LDAP groups to Django groups.
+        AUTH_LDAP_FIND_GROUP_PERMS = True
+      '';
     };
   };
 
   config = mkIf cfg.enable {
-    services.netbox.plugins = mkIf cfg.enableLdap (ps: [ ps.django-auth-ldap ]);
+    services.netbox = {
+      plugins = mkIf cfg.enableLdap (ps: [ ps.django-auth-ldap ]);
+      settings = {
+        STATIC_ROOT = staticDir;
+        MEDIA_ROOT = "${cfg.dataDir}/media";
+        REPORTS_ROOT = "${cfg.dataDir}/reports";
+        SCRIPTS_ROOT = "${cfg.dataDir}/scripts";
+
+        DATABASE = {
+          NAME = "netbox";
+          USER = "netbox";
+          HOST = "/run/postgresql";
+        };
+
+        # Redis database settings. Redis is used for caching and for queuing
+        # background tasks such as webhook events. A separate configuration
+        # exists for each. Full connection details are required in both
+        # sections, and it is strongly recommended to use two separate database
+        # IDs.
+        REDIS = {
+            tasks = {
+                URL = "unix://${config.services.redis.servers.netbox.unixSocket}?db=0";
+                SSL = false;
+            };
+            caching =  {
+                URL = "unix://${config.services.redis.servers.netbox.unixSocket}?db=1";
+                SSL = false;
+            };
+        };
+
+        REMOTE_AUTH_BACKEND = lib.mkIf cfg.enableLdap "netbox.authentication.LDAPBackend";
+
+        LOGGING = lib.mkDefault {
+          version = 1;
+
+          formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s";
+
+          handlers.console = {
+            class = "logging.StreamHandler";
+            formatter = "precise";
+          };
+
+          # log to console/systemd instead of file
+          root = {
+            level = "INFO";
+            handlers = [ "console" ];
+          };
+        };
+      };
+
+      extraConfig = ''
+        with open("${cfg.secretKeyFile}", "r") as file:
+            SECRET_KEY = file.readline()
+      '';
+    };
 
     services.redis.servers.netbox.enable = true;
 
@@ -175,6 +261,7 @@ in {
         StateDirectory = "netbox";
         StateDirectoryMode = "0750";
         Restart = "on-failure";
+        RestartSec = 30;
       };
     in {
       netbox-migration = {
@@ -190,13 +277,18 @@ in {
           ExecStart = ''
             ${pkg}/bin/netbox migrate
           '';
+          PrivateTmp = true;
         };
       };
 
       netbox = {
         description = "NetBox WSGI Service";
+        documentation = [ "https://docs.netbox.dev/" ];
+
         wantedBy = [ "netbox.target" ];
-        after = [ "netbox-migration.service" ];
+
+        after = [ "network-online.target" "netbox-migration.service" ];
+        wants = [ "network-online.target" ];
 
         preStart = ''
           ${pkg}/bin/netbox trace_paths --no-input
@@ -204,9 +296,7 @@ in {
           ${pkg}/bin/netbox remove_stale_contenttypes --no-input
         '';
 
-        environment = {
-          PYTHONPATH = pkg.pythonPath;
-        };
+        environment.PYTHONPATH = pkg.pythonPath;
 
         serviceConfig = defaultServiceConfig // {
           ExecStart = ''
@@ -214,32 +304,37 @@ in {
               --bind ${cfg.listenAddress}:${toString cfg.port} \
               --pythonpath ${pkg}/opt/netbox/netbox
           '';
+          PrivateTmp = true;
         };
       };
 
       netbox-rq = {
         description = "NetBox Request Queue Worker";
+        documentation = [ "https://docs.netbox.dev/" ];
+
         wantedBy = [ "netbox.target" ];
         after = [ "netbox.service" ];
 
-        environment = {
-          PYTHONPATH = pkg.pythonPath;
-        };
+        environment.PYTHONPATH = pkg.pythonPath;
 
         serviceConfig = defaultServiceConfig // {
           ExecStart = ''
             ${pkg}/bin/netbox rqworker high default low
           '';
+          PrivateTmp = true;
         };
       };
 
       netbox-housekeeping = {
         description = "NetBox housekeeping job";
-        after = [ "netbox.service" ];
+        documentation = [ "https://docs.netbox.dev/" ];
 
-        environment = {
-          PYTHONPATH = pkg.pythonPath;
-        };
+        wantedBy = [ "multi-user.target" ];
+
+        after = [ "network-online.target" ];
+        wants = [ "network-online.target" ];
+
+        environment.PYTHONPATH = pkg.pythonPath;
 
         serviceConfig = defaultServiceConfig // {
           Type = "oneshot";
@@ -252,10 +347,17 @@ in {
 
     systemd.timers.netbox-housekeeping = {
       description = "Run NetBox housekeeping job";
-      wantedBy = [ "timers.target" ];
+      documentation = [ "https://docs.netbox.dev/" ];
+
+      wantedBy = [ "multi-user.target" ];
+
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
 
       timerConfig = {
         OnCalendar = "daily";
+        AccuracySec = "1h";
+        Persistent = true;
       };
     };
 
diff --git a/nixos/modules/services/web-apps/nextcloud-notify_push.nix b/nixos/modules/services/web-apps/nextcloud-notify_push.nix
new file mode 100644
index 00000000000..759daa0c50d
--- /dev/null
+++ b/nixos/modules/services/web-apps/nextcloud-notify_push.nix
@@ -0,0 +1,123 @@
+{ config, options, lib, pkgs, ... }:
+
+let
+  cfg = config.services.nextcloud.notify_push;
+  cfgN = config.services.nextcloud;
+in
+{
+  options.services.nextcloud.notify_push = {
+    enable = lib.mkEnableOption (lib.mdDoc "Notify push");
+
+    package = lib.mkOption {
+      type = lib.types.package;
+      default = pkgs.nextcloud-notify_push;
+      defaultText = lib.literalMD "pkgs.nextcloud-notify_push";
+      description = lib.mdDoc "Which package to use for notify_push";
+    };
+
+    socketPath = lib.mkOption {
+      type = lib.types.str;
+      default = "/run/nextcloud-notify_push/sock";
+      description = lib.mdDoc "Socket path to use for notify_push";
+    };
+
+    logLevel = lib.mkOption {
+      type = lib.types.enum [ "error" "warn" "info" "debug" "trace" ];
+      default = "error";
+      description = lib.mdDoc "Log level";
+    };
+
+    bendDomainToLocalhost = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Whether to add an entry to `/etc/hosts` for the configured nextcloud domain to point to `localhost` and add `localhost `to nextcloud's `trusted_proxies` config option.
+
+        This is useful when nextcloud's domain is not a static IP address and when the reverse proxy cannot be bypassed because the backend connection is done via unix socket.
+      '';
+    };
+  } // (
+    lib.genAttrs [
+      "dbtype"
+      "dbname"
+      "dbuser"
+      "dbpassFile"
+      "dbhost"
+      "dbport"
+      "dbtableprefix"
+    ] (
+      opt: options.services.nextcloud.config.${opt} // {
+        default = config.services.nextcloud.config.${opt};
+        defaultText = "config.services.nextcloud.config.${opt}";
+      }
+    )
+  );
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.nextcloud-notify_push = let
+      nextcloudUrl = "http${lib.optionalString cfgN.https "s"}://${cfgN.hostName}";
+    in {
+      description = "Push daemon for Nextcloud clients";
+      documentation = [ "https://github.com/nextcloud/notify_push" ];
+      after = [
+        "phpfpm-nextcloud.service"
+        "redis-nextcloud.service"
+      ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        NEXTCLOUD_URL = nextcloudUrl;
+        SOCKET_PATH = cfg.socketPath;
+        DATABASE_PREFIX = cfg.dbtableprefix;
+        LOG = cfg.logLevel;
+      };
+      postStart = ''
+        ${cfgN.occ}/bin/nextcloud-occ notify_push:setup ${nextcloudUrl}/push
+      '';
+      script = let
+        dbType = if cfg.dbtype == "pgsql" then "postgresql" else cfg.dbtype;
+        dbUser = lib.optionalString (cfg.dbuser != null) cfg.dbuser;
+        dbPass = lib.optionalString (cfg.dbpassFile != null) ":$DATABASE_PASSWORD";
+        isSocket = lib.hasPrefix "/" (toString cfg.dbhost);
+        dbHost = lib.optionalString (cfg.dbhost != null) (if
+          isSocket then
+            if dbType == "postgresql" then "?host=${cfg.dbhost}" else
+            if dbType == "mysql" then "?socket=${cfg.dbhost}" else throw "unsupported dbtype"
+          else
+            "@${cfg.dbhost}");
+        dbName = lib.optionalString (cfg.dbname != null) "/${cfg.dbname}";
+        dbUrl = "${dbType}://${dbUser}${dbPass}${lib.optionalString (!isSocket) dbHost}${dbName}${lib.optionalString isSocket dbHost}";
+      in lib.optionalString (dbPass != "") ''
+        export DATABASE_PASSWORD="$(<"${cfg.dbpassFile}")"
+      '' + ''
+        export DATABASE_URL="${dbUrl}"
+        ${cfg.package}/bin/notify_push '${cfgN.datadir}/config/config.php'
+      '';
+      serviceConfig = {
+        User = "nextcloud";
+        Group = "nextcloud";
+        RuntimeDirectory = [ "nextcloud-notify_push" ];
+        Restart = "on-failure";
+        RestartSec = "5s";
+      };
+    };
+
+    networking.hosts = lib.mkIf cfg.bendDomainToLocalhost {
+      "127.0.0.1" = [ cfgN.hostName ];
+      "::1" = [ cfgN.hostName ];
+    };
+
+    services = lib.mkMerge [
+      {
+        nginx.virtualHosts.${cfgN.hostName}.locations."^~ /push/" = {
+          proxyPass = "http://unix:${cfg.socketPath}";
+          proxyWebsockets = true;
+          recommendedProxySettings = true;
+        };
+      }
+
+      (lib.mkIf cfg.bendDomainToLocalhost {
+        nextcloud.extraOptions.trusted_proxies = [ "127.0.0.1" "::1" ];
+      })
+    ];
+  };
+}
diff --git a/nixos/modules/services/web-apps/nextcloud.md b/nixos/modules/services/web-apps/nextcloud.md
new file mode 100644
index 00000000000..cbd7b5b3d06
--- /dev/null
+++ b/nixos/modules/services/web-apps/nextcloud.md
@@ -0,0 +1,227 @@
+# Nextcloud {#module-services-nextcloud}
+
+[Nextcloud](https://nextcloud.com/) is an open-source,
+self-hostable cloud platform. The server setup can be automated using
+[services.nextcloud](#opt-services.nextcloud.enable). A
+desktop client is packaged at `pkgs.nextcloud-client`.
+
+The current default by NixOS is `nextcloud27` which is also the latest
+major version available.
+
+## Basic usage {#module-services-nextcloud-basic-usage}
+
+Nextcloud is a PHP-based application which requires an HTTP server
+([`services.nextcloud`](#opt-services.nextcloud.enable)
+and optionally supports
+[`services.nginx`](#opt-services.nginx.enable)).
+
+For the database, you can set
+[`services.nextcloud.config.dbtype`](#opt-services.nextcloud.config.dbtype) to
+either `sqlite` (the default), `mysql`, or `pgsql`. The simplest is `sqlite`,
+which will be automatically created and managed by the application. For the
+last two, you can easily create a local database by setting
+[`services.nextcloud.database.createLocally`](#opt-services.nextcloud.database.createLocally)
+to `true`, Nextcloud will automatically be configured to connect to it through
+socket.
+
+A very basic configuration may look like this:
+```
+{ pkgs, ... }:
+{
+  services.nextcloud = {
+    enable = true;
+    hostName = "nextcloud.tld";
+    database.createLocally = true;
+    config = {
+      dbtype = "pgsql";
+      adminpassFile = "/path/to/admin-pass-file";
+    };
+  };
+
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+}
+```
+
+The `hostName` option is used internally to configure an HTTP
+server using [`PHP-FPM`](https://php-fpm.org/)
+and `nginx`. The `config` attribute set is
+used by the imperative installer and all values are written to an additional file
+to ensure that changes can be applied by changing the module's options.
+
+In case the application serves multiple domains (those are checked with
+[`$_SERVER['HTTP_HOST']`](http://php.net/manual/en/reserved.variables.server.php))
+it's needed to add them to
+[`services.nextcloud.config.extraTrustedDomains`](#opt-services.nextcloud.config.extraTrustedDomains).
+
+Auto updates for Nextcloud apps can be enabled using
+[`services.nextcloud.autoUpdateApps`](#opt-services.nextcloud.autoUpdateApps.enable).
+
+## Common problems {#module-services-nextcloud-pitfalls-during-upgrade}
+
+  - **General notes.**
+    Unfortunately Nextcloud appears to be very stateful when it comes to
+    managing its own configuration. The config file lives in the home directory
+    of the `nextcloud` user (by default
+    `/var/lib/nextcloud/config/config.php`) and is also used to
+    track several states of the application (e.g., whether installed or not).
+
+     All configuration parameters are also stored in
+    {file}`/var/lib/nextcloud/config/override.config.php` which is generated by
+    the module and linked from the store to ensure that all values from
+    {file}`config.php` can be modified by the module.
+    However {file}`config.php` manages the application's state and shouldn't be
+    touched manually because of that.
+
+    ::: {.warning}
+    Don't delete {file}`config.php`! This file
+    tracks the application's state and a deletion can cause unwanted
+    side-effects!
+    :::
+
+    ::: {.warning}
+    Don't rerun `nextcloud-occ maintenance:install`!
+    This command tries to install the application
+    and can cause unwanted side-effects!
+    :::
+  - **Multiple version upgrades.**
+    Nextcloud doesn't allow to move more than one major-version forward. E.g., if you're on
+    `v16`, you cannot upgrade to `v18`, you need to upgrade to
+    `v17` first. This is ensured automatically as long as the
+    [stateVersion](#opt-system.stateVersion) is declared properly. In that case
+    the oldest version available (one major behind the one from the previous NixOS
+    release) will be selected by default and the module will generate a warning that reminds
+    the user to upgrade to latest Nextcloud *after* that deploy.
+  - **`Error: Command "upgrade" is not defined.`**
+    This error usually occurs if the initial installation
+    ({command}`nextcloud-occ maintenance:install`) has failed. After that, the application
+    is not installed, but the upgrade is attempted to be executed. Further context can
+    be found in [NixOS/nixpkgs#111175](https://github.com/NixOS/nixpkgs/issues/111175).
+
+    First of all, it makes sense to find out what went wrong by looking at the logs
+    of the installation via {command}`journalctl -u nextcloud-setup` and try to fix
+    the underlying issue.
+
+    - If this occurs on an *existing* setup, this is most likely because
+      the maintenance mode is active. It can be deactivated by running
+      {command}`nextcloud-occ maintenance:mode --off`. It's advisable though to
+      check the logs first on why the maintenance mode was activated.
+    - ::: {.warning}
+      Only perform the following measures on
+      *freshly installed instances!*
+      :::
+
+      A re-run of the installer can be forced by *deleting*
+      {file}`/var/lib/nextcloud/config/config.php`. This is the only time
+      advisable because the fresh install doesn't have any state that can be lost.
+      In case that doesn't help, an entire re-creation can be forced via
+      {command}`rm -rf ~nextcloud/`.
+
+  - **Server-side encryption.**
+    Nextcloud supports [server-side encryption (SSE)](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html).
+    This is not an end-to-end encryption, but can be used to encrypt files that will be persisted
+    to external storage such as S3. Please note that this won't work anymore when using OpenSSL 3
+    for PHP's openssl extension and **Nextcloud 25 or older** because this is implemented using the
+    legacy cipher RC4. For Nextcloud26 this isn't relevant anymore, because Nextcloud has an RC4 implementation
+    written in native PHP and thus doesn't need `ext-openssl` for that anymore.
+    If [](#opt-system.stateVersion) is *above* `22.05`,
+    this is disabled by default. To turn it on again and for further information please refer to
+    [](#opt-services.nextcloud.enableBrokenCiphersForSSE).
+
+## Using an alternative webserver as reverse-proxy (e.g. `httpd`) {#module-services-nextcloud-httpd}
+
+By default, `nginx` is used as reverse-proxy for `nextcloud`.
+However, it's possible to use e.g. `httpd` by explicitly disabling
+`nginx` using [](#opt-services.nginx.enable) and fixing the
+settings `listen.owner` &amp; `listen.group` in the
+[corresponding `phpfpm` pool](#opt-services.phpfpm.pools).
+
+An exemplary configuration may look like this:
+```
+{ config, lib, pkgs, ... }: {
+  services.nginx.enable = false;
+  services.nextcloud = {
+    enable = true;
+    hostName = "localhost";
+
+    /* further, required options */
+  };
+  services.phpfpm.pools.nextcloud.settings = {
+    "listen.owner" = config.services.httpd.user;
+    "listen.group" = config.services.httpd.group;
+  };
+  services.httpd = {
+    enable = true;
+    adminAddr = "webmaster@localhost";
+    extraModules = [ "proxy_fcgi" ];
+    virtualHosts."localhost" = {
+      documentRoot = config.services.nextcloud.package;
+      extraConfig = ''
+        <Directory "${config.services.nextcloud.package}">
+          <FilesMatch "\.php$">
+            <If "-f %{REQUEST_FILENAME}">
+              SetHandler "proxy:unix:${config.services.phpfpm.pools.nextcloud.socket}|fcgi://localhost/"
+            </If>
+          </FilesMatch>
+          <IfModule mod_rewrite.c>
+            RewriteEngine On
+            RewriteBase /
+            RewriteRule ^index\.php$ - [L]
+            RewriteCond %{REQUEST_FILENAME} !-f
+            RewriteCond %{REQUEST_FILENAME} !-d
+            RewriteRule . /index.php [L]
+          </IfModule>
+          DirectoryIndex index.php
+          Require all granted
+          Options +FollowSymLinks
+        </Directory>
+      '';
+    };
+  };
+}
+```
+
+## Installing Apps and PHP extensions {#installing-apps-php-extensions-nextcloud}
+
+Nextcloud apps are installed statefully through the web interface.
+Some apps may require extra PHP extensions to be installed.
+This can be configured with the [](#opt-services.nextcloud.phpExtraExtensions) setting.
+
+Alternatively, extra apps can also be declared with the [](#opt-services.nextcloud.extraApps) setting.
+When using this setting, apps can no longer be managed statefully because this can lead to Nextcloud updating apps
+that are managed by Nix. If you want automatic updates it is recommended that you use web interface to install apps.
+
+## Maintainer information {#module-services-nextcloud-maintainer-info}
+
+As stated in the previous paragraph, we must provide a clean upgrade-path for Nextcloud
+since it cannot move more than one major version forward on a single upgrade. This chapter
+adds some notes how Nextcloud updates should be rolled out in the future.
+
+While minor and patch-level updates are no problem and can be done directly in the
+package-expression (and should be backported to supported stable branches after that),
+major-releases should be added in a new attribute (e.g. Nextcloud `v19.0.0`
+should be available in `nixpkgs` as `pkgs.nextcloud19`).
+To provide simple upgrade paths it's generally useful to backport those as well to stable
+branches. As long as the package-default isn't altered, this won't break existing setups.
+After that, the versioning-warning in the `nextcloud`-module should be
+updated to make sure that the
+[package](#opt-services.nextcloud.package)-option selects the latest version
+on fresh setups.
+
+If major-releases will be abandoned by upstream, we should check first if those are needed
+in NixOS for a safe upgrade-path before removing those. In that case we should keep those
+packages, but mark them as insecure in an expression like this (in
+`<nixpkgs/pkgs/servers/nextcloud/default.nix>`):
+```
+/* ... */
+{
+  nextcloud17 = generic {
+    version = "17.0.x";
+    sha256 = "0000000000000000000000000000000000000000000000000000";
+    eol = true;
+  };
+}
+```
+
+Ideally we should make sure that it's possible to jump two NixOS versions forward:
+i.e. the warnings and the logic in the module should guard a user to upgrade from a
+Nextcloud on e.g. 19.09 to a Nextcloud on 20.09.
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index 90801e99681..06af9d933e0 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -57,6 +57,9 @@ let
 
   inherit (config.system) stateVersion;
 
+  mysqlLocal = cfg.database.createLocally && cfg.config.dbtype == "mysql";
+  pgsqlLocal = cfg.database.createLocally && cfg.config.dbtype == "pgsql";
+
 in {
 
   imports = [
@@ -79,7 +82,7 @@ in {
       (which can be opened e.g. by running `nixos-help`).
     '')
     (mkRemovedOptionModule [ "services" "nextcloud" "disableImagemagick" ] ''
-      Use services.nextcloud.nginx.enableImagemagick instead.
+      Use services.nextcloud.enableImagemagick instead.
     '')
   ];
 
@@ -204,11 +207,11 @@ in {
     package = mkOption {
       type = types.package;
       description = lib.mdDoc "Which package to use for the Nextcloud instance.";
-      relatedPackages = [ "nextcloud24" "nextcloud25" ];
+      relatedPackages = [ "nextcloud25" "nextcloud26" "nextcloud27" ];
     };
     phpPackage = mkOption {
       type = types.package;
-      relatedPackages = [ "php80" "php81" ];
+      relatedPackages = [ "php81" "php82" ];
       defaultText = "pkgs.php";
       description = lib.mdDoc ''
         PHP package to use for Nextcloud.
@@ -316,11 +319,7 @@ in {
         type = types.bool;
         default = false;
         description = lib.mdDoc ''
-          Create the database and database user locally. Only available for
-          mysql database.
-          Note that this option will use the latest version of MariaDB which
-          is not officially supported by Nextcloud. As for now a workaround
-          is used to also support MariaDB version >= 10.6.
+          Create the database and database user locally.
         '';
       };
 
@@ -352,12 +351,15 @@ in {
       };
       dbhost = mkOption {
         type = types.nullOr types.str;
-        default = "localhost";
+        default =
+          if pgsqlLocal then "/run/postgresql"
+          else if mysqlLocal then "localhost:/run/mysqld/mysqld.sock"
+          else "localhost";
+        defaultText = "localhost";
         description = lib.mdDoc ''
-          Database host.
-
-          Note: for using Unix authentication with PostgreSQL, this should be
-          set to `/run/postgresql`.
+          Database host or socket path. Defaults to the correct unix socket
+          instead if `services.nextcloud.database.createLocally` is true and
+          `services.nextcloud.config.dbtype` is either `pgsql` or `mysql`.
         '';
       };
       dbport = mkOption {
@@ -379,7 +381,8 @@ in {
         type = types.str;
         description = lib.mdDoc ''
           The full path to a file that contains the admin's password. Must be
-          readable by user `nextcloud`.
+          readable by user `nextcloud`. The password is set only in the initial
+          setup of nextcloud by the systemd `nextcloud-setup.service`.
         '';
       };
 
@@ -514,6 +517,27 @@ in {
               `http://hostname.domain/bucket` instead.
             '';
           };
+          sseCKeyFile = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            example = "/var/nextcloud-objectstore-s3-sse-c-key";
+            description = lib.mdDoc ''
+              If provided this is the full path to a file that contains the key
+              to enable [server-side encryption with customer-provided keys][1]
+              (SSE-C).
+
+              The file must contain a random 32-byte key encoded as a base64
+              string, e.g. generated with the command
+
+              ```
+              openssl rand 32 | base64
+              ```
+
+              Must be readable by user `nextcloud`.
+
+              [1]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html
+            '';
+          };
         };
       };
     };
@@ -528,6 +552,19 @@ in {
       default = true;
     };
 
+    configureRedis = lib.mkOption {
+      type = lib.types.bool;
+      default = config.services.nextcloud.notify_push.enable;
+      defaultText = literalExpression "config.services.nextcloud.notify_push.enable";
+      description = lib.mdDoc ''
+        Whether to configure nextcloud to use the recommended redis settings for small instances.
+
+        ::: {.note}
+        The `notify_push` app requires redis to be configured. If this option is turned off, this must be configured manually.
+        :::
+      '';
+    };
+
     caching = {
       apcu = mkOption {
         type = types.bool;
@@ -652,7 +689,7 @@ in {
 
   config = mkIf cfg.enable (mkMerge [
     { warnings = let
-        latest = 25;
+        latest = 27;
         upgradeWarning = major: nixos:
           ''
             A legacy Nextcloud install (from before NixOS ${nixos}) may be installed.
@@ -667,27 +704,13 @@ in {
             `services.nextcloud.package`.
           '';
 
-        # FIXME(@Ma27) remove as soon as nextcloud properly supports
-        # mariadb >=10.6.
-        isUnsupportedMariadb =
-          # All currently supported Nextcloud versions are affected (https://github.com/nextcloud/server/issues/25436).
-          (versionOlder cfg.package.version "24")
-          # This module uses mysql
-          && (cfg.config.dbtype == "mysql")
-          # MySQL is managed via NixOS
-          && config.services.mysql.enable
-          # We're using MariaDB
-          && (getName config.services.mysql.package) == "mariadb-server"
-          # MariaDB is at least 10.6 and thus not supported
-          && (versionAtLeast (getVersion config.services.mysql.package) "10.6");
-
       in (optional (cfg.poolConfig != null) ''
           Using config.services.nextcloud.poolConfig is deprecated and will become unsupported in a future release.
           Please migrate your configuration to config.services.nextcloud.poolSettings.
         '')
-        ++ (optional (versionOlder cfg.package.version "23") (upgradeWarning 22 "22.05"))
-        ++ (optional (versionOlder cfg.package.version "24") (upgradeWarning 23 "22.05"))
         ++ (optional (versionOlder cfg.package.version "25") (upgradeWarning 24 "22.11"))
+        ++ (optional (versionOlder cfg.package.version "26") (upgradeWarning 25 "23.05"))
+        ++ (optional (versionOlder cfg.package.version "27") (upgradeWarning 26 "23.11"))
         ++ (optional cfg.enableBrokenCiphersForSSE ''
           You're using PHP's openssl extension built against OpenSSL 1.1 for Nextcloud.
           This is only necessary if you're using Nextcloud's server-side encryption.
@@ -705,17 +728,10 @@ in {
 
           For more context, here is the implementing pull request: https://github.com/NixOS/nixpkgs/pull/198470
         '')
-        ++ (optional isUnsupportedMariadb ''
-            You seem to be using MariaDB at an unsupported version (i.e. at least 10.6)!
-            Please note that this isn't supported officially by Nextcloud. You can either
-
-            * Switch to `pkgs.mysql`
-            * Downgrade MariaDB to at least 10.5
-            * Work around Nextcloud's problems by specifying `innodb_read_only_compressed=0`
-
-            For further context, please read
-            https://help.nextcloud.com/t/update-to-next-cloud-21-0-2-has-get-an-error/117028/15
-          '');
+        ++ (optional (cfg.enableBrokenCiphersForSSE && versionAtLeast cfg.package.version "26") ''
+          Nextcloud26 supports RC4 without requiring legacy OpenSSL, so
+          `services.nextcloud.enableBrokenCiphersForSSE` can be set to `false`.
+        '');
 
       services.nextcloud.package = with pkgs;
         mkDefault (
@@ -726,17 +742,32 @@ in {
               `pkgs.nextcloud`.
             ''
           else if versionOlder stateVersion "22.11" then nextcloud24
-          else nextcloud25
+          else if versionOlder stateVersion "23.05" then nextcloud25
+          else if versionOlder stateVersion "23.11" then nextcloud26
+          else nextcloud27
         );
 
       services.nextcloud.phpPackage =
-        if versionOlder cfg.package.version "24" then pkgs.php80
-        else pkgs.php81;
+        if versionOlder cfg.package.version "26" then pkgs.php81
+        else pkgs.php82;
     }
 
     { assertions = [
-      { assertion = cfg.database.createLocally -> cfg.config.dbtype == "mysql";
-        message = ''services.nextcloud.config.dbtype must be set to mysql if services.nextcloud.database.createLocally is set to true.'';
+      { assertion = cfg.database.createLocally -> cfg.config.dbpassFile == null;
+        message = ''
+          Using `services.nextcloud.database.createLocally` with database
+          password authentication is no longer supported.
+
+          If you use an external database (or want to use password auth for any
+          other reason), set `services.nextcloud.database.createLocally` to
+          `false`. The database won't be managed for you (use `services.mysql`
+          if you want to set it up).
+
+          If you want this module to manage your nextcloud database for you,
+          unset `services.nextcloud.config.dbpassFile` and
+          `services.nextcloud.config.dbhost` to use socket authentication
+          instead of password.
+        '';
       }
     ]; }
 
@@ -773,6 +804,7 @@ in {
                 'use_ssl' => ${boolToString s3.useSsl},
                 ${optionalString (s3.region != null) "'region' => '${s3.region}',"}
                 'use_path_style' => ${boolToString s3.usePathStyle},
+                ${optionalString (s3.sseCKeyFile != null) "'sse_c_key' => nix_read_secret('${s3.sseCKeyFile}'),"}
               ],
             ]
           '';
@@ -899,6 +931,8 @@ in {
         in {
           wantedBy = [ "multi-user.target" ];
           before = [ "phpfpm-nextcloud.service" ];
+          after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+          requires = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
           path = [ occ ];
           script = ''
             ${optionalString (c.dbpassFile != null) ''
@@ -958,6 +992,9 @@ in {
           '';
           serviceConfig.Type = "oneshot";
           serviceConfig.User = "nextcloud";
+          # On Nextcloud ≥ 26, it is not necessary to patch the database files to prevent
+          # an automatic creation of the database user.
+          environment.NC_setup_create_db_user = lib.mkIf (nextcloudGreaterOrEqualThan "26") "false";
         };
         nextcloud-cron = {
           after = [ "nextcloud-setup.service" ];
@@ -1001,7 +1038,7 @@ in {
 
       environment.systemPackages = [ occ ];
 
-      services.mysql = lib.mkIf cfg.database.createLocally {
+      services.mysql = lib.mkIf mysqlLocal {
         enable = true;
         package = lib.mkDefault pkgs.mariadb;
         ensureDatabases = [ cfg.config.dbname ];
@@ -1009,22 +1046,32 @@ in {
           name = cfg.config.dbuser;
           ensurePermissions = { "${cfg.config.dbname}.*" = "ALL PRIVILEGES"; };
         }];
-        # FIXME(@Ma27) Nextcloud isn't compatible with mariadb 10.6,
-        # this is a workaround.
-        # See https://help.nextcloud.com/t/update-to-next-cloud-21-0-2-has-get-an-error/117028/22
-        settings = mkIf (versionOlder cfg.package.version "24") {
-          mysqld = {
-            innodb_read_only_compressed = 0;
+      };
+
+      services.postgresql = mkIf pgsqlLocal {
+        enable = true;
+        ensureDatabases = [ cfg.config.dbname ];
+        ensureUsers = [{
+          name = cfg.config.dbuser;
+          ensurePermissions = { "DATABASE ${cfg.config.dbname}" = "ALL PRIVILEGES"; };
+        }];
+      };
+
+      services.redis.servers.nextcloud = lib.mkIf cfg.configureRedis {
+        enable = true;
+        user = "nextcloud";
+      };
+
+      services.nextcloud = lib.mkIf cfg.configureRedis {
+        caching.redis = true;
+        extraOptions = {
+          "memcache.distributed" = ''\OC\Memcache\Redis'';
+          "memcache.locking" = ''\OC\Memcache\Redis'';
+          redis = {
+            host = config.services.redis.servers.nextcloud.unixSocket;
+            port = 0;
           };
         };
-        initialScript = pkgs.writeText "mysql-init" ''
-          CREATE USER '${cfg.config.dbname}'@'localhost' IDENTIFIED BY '${builtins.readFile( cfg.config.dbpassFile )}';
-          CREATE DATABASE IF NOT EXISTS ${cfg.config.dbname};
-          GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER,
-            CREATE TEMPORARY TABLES ON ${cfg.config.dbname}.* TO '${cfg.config.dbuser}'@'localhost'
-            IDENTIFIED BY '${builtins.readFile( cfg.config.dbpassFile )}';
-          FLUSH privileges;
-        '';
       };
 
       services.nginx.enable = mkDefault true;
@@ -1118,7 +1165,7 @@ in {
           ${optionalString (cfg.nginx.recommendedHttpHeaders) ''
             add_header X-Content-Type-Options nosniff;
             add_header X-XSS-Protection "1; mode=block";
-            add_header X-Robots-Tag none;
+            add_header X-Robots-Tag "noindex, nofollow";
             add_header X-Download-Options noopen;
             add_header X-Permitted-Cross-Domain-Policies none;
             add_header X-Frame-Options sameorigin;
@@ -1146,5 +1193,5 @@ in {
     }
   ]);
 
-  meta.doc = ./nextcloud.xml;
+  meta.doc = ./nextcloud.md;
 }
diff --git a/nixos/modules/services/web-apps/nextcloud.xml b/nixos/modules/services/web-apps/nextcloud.xml
deleted file mode 100644
index 4207c4008d5..00000000000
--- a/nixos/modules/services/web-apps/nextcloud.xml
+++ /dev/null
@@ -1,305 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-nextcloud">
- <title>Nextcloud</title>
- <para>
-  <link xlink:href="https://nextcloud.com/">Nextcloud</link> is an open-source,
-  self-hostable cloud platform. The server setup can be automated using
-  <link linkend="opt-services.nextcloud.enable">services.nextcloud</link>. A
-  desktop client is packaged at <literal>pkgs.nextcloud-client</literal>.
- </para>
- <para>
-  The current default by NixOS is <package>nextcloud25</package> which is also the latest
-  major version available.
- </para>
- <section xml:id="module-services-nextcloud-basic-usage">
-  <title>Basic usage</title>
-
-  <para>
-   Nextcloud is a PHP-based application which requires an HTTP server
-   (<literal><link linkend="opt-services.nextcloud.enable">services.nextcloud</link></literal>
-   optionally supports
-   <literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>)
-   and a database (it's recommended to use
-   <literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal>).
-  </para>
-
-  <para>
-   A very basic configuration may look like this:
-<programlisting>{ pkgs, ... }:
-{
-  services.nextcloud = {
-    <link linkend="opt-services.nextcloud.enable">enable</link> = true;
-    <link linkend="opt-services.nextcloud.hostName">hostName</link> = "nextcloud.tld";
-    config = {
-      <link linkend="opt-services.nextcloud.config.dbtype">dbtype</link> = "pgsql";
-      <link linkend="opt-services.nextcloud.config.dbuser">dbuser</link> = "nextcloud";
-      <link linkend="opt-services.nextcloud.config.dbhost">dbhost</link> = "/run/postgresql"; # nextcloud will add /.s.PGSQL.5432 by itself
-      <link linkend="opt-services.nextcloud.config.dbname">dbname</link> = "nextcloud";
-      <link linkend="opt-services.nextcloud.config.adminpassFile">adminpassFile</link> = "/path/to/admin-pass-file";
-      <link linkend="opt-services.nextcloud.config.adminuser">adminuser</link> = "root";
-    };
-  };
-
-  services.postgresql = {
-    <link linkend="opt-services.postgresql.enable">enable</link> = true;
-    <link linkend="opt-services.postgresql.ensureDatabases">ensureDatabases</link> = [ "nextcloud" ];
-    <link linkend="opt-services.postgresql.ensureUsers">ensureUsers</link> = [
-     { name = "nextcloud";
-       ensurePermissions."DATABASE nextcloud" = "ALL PRIVILEGES";
-     }
-    ];
-  };
-
-  # ensure that postgres is running *before* running the setup
-  systemd.services."nextcloud-setup" = {
-    requires = ["postgresql.service"];
-    after = ["postgresql.service"];
-  };
-
-  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-}</programlisting>
-  </para>
-
-  <para>
-   The <literal>hostName</literal> option is used internally to configure an HTTP
-   server using <literal><link xlink:href="https://php-fpm.org/">PHP-FPM</link></literal>
-   and <literal>nginx</literal>. The <literal>config</literal> attribute set is
-   used by the imperative installer and all values are written to an additional file
-   to ensure that changes can be applied by changing the module's options.
-  </para>
-
-  <para>
-   In case the application serves multiple domains (those are checked with
-   <literal><link xlink:href="http://php.net/manual/en/reserved.variables.server.php">$_SERVER['HTTP_HOST']</link></literal>)
-   it's needed to add them to
-   <literal><link linkend="opt-services.nextcloud.config.extraTrustedDomains">services.nextcloud.config.extraTrustedDomains</link></literal>.
-  </para>
-
-  <para>
-   Auto updates for Nextcloud apps can be enabled using
-   <literal><link linkend="opt-services.nextcloud.autoUpdateApps.enable">services.nextcloud.autoUpdateApps</link></literal>.
-</para>
-
- </section>
-
- <section xml:id="module-services-nextcloud-pitfalls-during-upgrade">
-  <title>Common problems</title>
-  <itemizedlist>
-   <listitem>
-    <formalpara>
-     <title>General notes</title>
-     <para>
-      Unfortunately Nextcloud appears to be very stateful when it comes to
-      managing its own configuration. The config file lives in the home directory
-      of the <literal>nextcloud</literal> user (by default
-      <literal>/var/lib/nextcloud/config/config.php</literal>) and is also used to
-      track several states of the application (e.g., whether installed or not).
-     </para>
-    </formalpara>
-    <para>
-     All configuration parameters are also stored in
-     <filename>/var/lib/nextcloud/config/override.config.php</filename> which is generated by
-     the module and linked from the store to ensure that all values from
-     <filename>config.php</filename> can be modified by the module.
-     However <filename>config.php</filename> manages the application's state and shouldn't be
-     touched manually because of that.
-    </para>
-    <warning>
-     <para>Don't delete <filename>config.php</filename>! This file
-     tracks the application's state and a deletion can cause unwanted
-     side-effects!</para>
-    </warning>
-
-    <warning>
-     <para>Don't rerun <literal>nextcloud-occ
-     maintenance:install</literal>! This command tries to install the application
-     and can cause unwanted side-effects!</para>
-    </warning>
-   </listitem>
-   <listitem>
-    <formalpara>
-     <title>Multiple version upgrades</title>
-     <para>
-      Nextcloud doesn't allow to move more than one major-version forward. E.g., if you're on
-      <literal>v16</literal>, you cannot upgrade to <literal>v18</literal>, you need to upgrade to
-      <literal>v17</literal> first. This is ensured automatically as long as the
-      <link linkend="opt-system.stateVersion">stateVersion</link> is declared properly. In that case
-      the oldest version available (one major behind the one from the previous NixOS
-      release) will be selected by default and the module will generate a warning that reminds
-      the user to upgrade to latest Nextcloud <emphasis>after</emphasis> that deploy.
-     </para>
-    </formalpara>
-   </listitem>
-   <listitem>
-    <formalpara>
-     <title><literal>Error: Command "upgrade" is not defined.</literal></title>
-     <para>
-      This error usually occurs if the initial installation
-      (<command>nextcloud-occ maintenance:install</command>) has failed. After that, the application
-      is not installed, but the upgrade is attempted to be executed. Further context can
-      be found in <link xlink:href="https://github.com/NixOS/nixpkgs/issues/111175">NixOS/nixpkgs#111175</link>.
-     </para>
-    </formalpara>
-    <para>
-     First of all, it makes sense to find out what went wrong by looking at the logs
-     of the installation via <command>journalctl -u nextcloud-setup</command> and try to fix
-     the underlying issue.
-    </para>
-    <itemizedlist>
-     <listitem>
-      <para>
-       If this occurs on an <emphasis>existing</emphasis> setup, this is most likely because
-       the maintenance mode is active. It can be deactivated by running
-       <command>nextcloud-occ maintenance:mode --off</command>. It's advisable though to
-       check the logs first on why the maintenance mode was activated.
-      </para>
-     </listitem>
-     <listitem>
-      <warning><para>Only perform the following measures on
-      <emphasis>freshly installed instances!</emphasis></para></warning>
-      <para>
-       A re-run of the installer can be forced by <emphasis>deleting</emphasis>
-       <filename>/var/lib/nextcloud/config/config.php</filename>. This is the only time
-       advisable because the fresh install doesn't have any state that can be lost.
-       In case that doesn't help, an entire re-creation can be forced via
-       <command>rm -rf ~nextcloud/</command>.
-      </para>
-     </listitem>
-    </itemizedlist>
-   </listitem>
-   <listitem>
-    <formalpara>
-     <title>Server-side encryption</title>
-     <para>
-      Nextcloud supports <link xlink:href="https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html">server-side encryption (SSE)</link>.
-      This is not an end-to-end encryption, but can be used to encrypt files that will be persisted
-      to external storage such as S3. Please note that this won't work anymore when using OpenSSL 3
-      for PHP's openssl extension because this is implemented using the legacy cipher RC4.
-      If <xref linkend="opt-system.stateVersion" /> is <emphasis>above</emphasis> <literal>22.05</literal>,
-      this is disabled by default. To turn it on again and for further information please refer to
-      <xref linkend="opt-services.nextcloud.enableBrokenCiphersForSSE" />.
-     </para>
-    </formalpara>
-   </listitem>
-  </itemizedlist>
- </section>
-
- <section xml:id="module-services-nextcloud-httpd">
-  <title>Using an alternative webserver as reverse-proxy (e.g. <literal>httpd</literal>)</title>
-  <para>
-   By default, <package>nginx</package> is used as reverse-proxy for <package>nextcloud</package>.
-   However, it's possible to use e.g. <package>httpd</package> by explicitly disabling
-   <package>nginx</package> using <xref linkend="opt-services.nginx.enable" /> and fixing the
-   settings <literal>listen.owner</literal> &amp; <literal>listen.group</literal> in the
-   <link linkend="opt-services.phpfpm.pools">corresponding <literal>phpfpm</literal> pool</link>.
-  </para>
-  <para>
-   An exemplary configuration may look like this:
-<programlisting>{ config, lib, pkgs, ... }: {
-  <link linkend="opt-services.nginx.enable">services.nginx.enable</link> = false;
-  services.nextcloud = {
-    <link linkend="opt-services.nextcloud.enable">enable</link> = true;
-    <link linkend="opt-services.nextcloud.hostName">hostName</link> = "localhost";
-
-    /* further, required options */
-  };
-  <link linkend="opt-services.phpfpm.pools._name_.settings">services.phpfpm.pools.nextcloud.settings</link> = {
-    "listen.owner" = config.services.httpd.user;
-    "listen.group" = config.services.httpd.group;
-  };
-  services.httpd = {
-    <link linkend="opt-services.httpd.enable">enable</link> = true;
-    <link linkend="opt-services.httpd.adminAddr">adminAddr</link> = "webmaster@localhost";
-    <link linkend="opt-services.httpd.extraModules">extraModules</link> = [ "proxy_fcgi" ];
-    virtualHosts."localhost" = {
-      <link linkend="opt-services.httpd.virtualHosts._name_.documentRoot">documentRoot</link> = config.services.nextcloud.package;
-      <link linkend="opt-services.httpd.virtualHosts._name_.extraConfig">extraConfig</link> = ''
-        &lt;Directory "${config.services.nextcloud.package}"&gt;
-          &lt;FilesMatch "\.php$"&gt;
-            &lt;If "-f %{REQUEST_FILENAME}"&gt;
-              SetHandler "proxy:unix:${config.services.phpfpm.pools.nextcloud.socket}|fcgi://localhost/"
-            &lt;/If&gt;
-          &lt;/FilesMatch&gt;
-          &lt;IfModule mod_rewrite.c&gt;
-            RewriteEngine On
-            RewriteBase /
-            RewriteRule ^index\.php$ - [L]
-            RewriteCond %{REQUEST_FILENAME} !-f
-            RewriteCond %{REQUEST_FILENAME} !-d
-            RewriteRule . /index.php [L]
-          &lt;/IfModule&gt;
-          DirectoryIndex index.php
-          Require all granted
-          Options +FollowSymLinks
-        &lt;/Directory&gt;
-      '';
-    };
-  };
-}</programlisting>
-  </para>
- </section>
-
- <section xml:id="installing-apps-php-extensions-nextcloud">
-  <title>Installing Apps and PHP extensions</title>
-
-  <para>
-   Nextcloud apps are installed statefully through the web interface.
-
-   Some apps may require extra PHP extensions to be installed.
-   This can be configured with the <xref linkend="opt-services.nextcloud.phpExtraExtensions" /> setting.
-  </para>
-
-  <para>
-   Alternatively, extra apps can also be declared with the <xref linkend="opt-services.nextcloud.extraApps" /> setting.
-   When using this setting, apps can no longer be managed statefully because this can lead to Nextcloud updating apps
-   that are managed by Nix. If you want automatic updates it is recommended that you use web interface to install apps.
-  </para>
- </section>
-
- <section xml:id="module-services-nextcloud-maintainer-info">
-  <title>Maintainer information</title>
-
-  <para>
-   As stated in the previous paragraph, we must provide a clean upgrade-path for Nextcloud
-   since it cannot move more than one major version forward on a single upgrade. This chapter
-   adds some notes how Nextcloud updates should be rolled out in the future.
-  </para>
-
-  <para>
-   While minor and patch-level updates are no problem and can be done directly in the
-   package-expression (and should be backported to supported stable branches after that),
-   major-releases should be added in a new attribute (e.g. Nextcloud <literal>v19.0.0</literal>
-   should be available in <literal>nixpkgs</literal> as <literal>pkgs.nextcloud19</literal>).
-   To provide simple upgrade paths it's generally useful to backport those as well to stable
-   branches. As long as the package-default isn't altered, this won't break existing setups.
-   After that, the versioning-warning in the <literal>nextcloud</literal>-module should be
-   updated to make sure that the
-   <link linkend="opt-services.nextcloud.package">package</link>-option selects the latest version
-   on fresh setups.
-  </para>
-
-  <para>
-   If major-releases will be abandoned by upstream, we should check first if those are needed
-   in NixOS for a safe upgrade-path before removing those. In that case we should keep those
-   packages, but mark them as insecure in an expression like this (in
-   <literal>&lt;nixpkgs/pkgs/servers/nextcloud/default.nix&gt;</literal>):
-<programlisting>/* ... */
-{
-  nextcloud17 = generic {
-    version = "17.0.x";
-    sha256 = "0000000000000000000000000000000000000000000000000000";
-    eol = true;
-  };
-}</programlisting>
-  </para>
-
-  <para>
-   Ideally we should make sure that it's possible to jump two NixOS versions forward:
-   i.e. the warnings and the logic in the module should guard a user to upgrade from a
-   Nextcloud on e.g. 19.09 to a Nextcloud on 20.09.
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/nifi.nix b/nixos/modules/services/web-apps/nifi.nix
index f643e24d81d..5ce56107783 100644
--- a/nixos/modules/services/web-apps/nifi.nix
+++ b/nixos/modules/services/web-apps/nifi.nix
@@ -13,9 +13,7 @@ let
 
   envFile = pkgs.writeText "nifi.env" (lib.concatMapStrings (s: s + "\n") (
     (lib.concatLists (lib.mapAttrsToList (name: value:
-      if value != null then [
-        "${name}=\"${toString value}\""
-      ] else []
+      lib.optional (value != null) ''${name}="${toString value}"''
     ) env))));
 
   nifiEnv = pkgs.writeShellScriptBin "nifi-env" ''
diff --git a/nixos/modules/services/web-apps/onlyoffice.nix b/nixos/modules/services/web-apps/onlyoffice.nix
index 79ed3e43dd1..3494f2fa21f 100644
--- a/nixos/modules/services/web-apps/onlyoffice.nix
+++ b/nixos/modules/services/web-apps/onlyoffice.nix
@@ -229,6 +229,9 @@ in
             cp -r ${cfg.package}/etc/onlyoffice/documentserver/* /run/onlyoffice/config/
             chmod u+w /run/onlyoffice/config/default.json
 
+            # Allow members of the onlyoffice group to serve files under /var/lib/onlyoffice/documentserver/App_Data
+            chmod g+x /var/lib/onlyoffice/documentserver
+
             cp /run/onlyoffice/config/default.json{,.orig}
 
             # for a mapping of environment variables from the docker container to json options see
@@ -267,7 +270,7 @@ in
           wantedBy = [ "multi-user.target" ];
           serviceConfig = {
             ExecStart = "${cfg.package.fhs}/bin/onlyoffice-wrapper DocService/docservice /run/onlyoffice/config";
-            ExecStartPre = onlyoffice-prestart;
+            ExecStartPre = [ onlyoffice-prestart ];
             Group = "onlyoffice";
             Restart = "always";
             RuntimeDirectory = "onlyoffice";
@@ -284,6 +287,8 @@ in
         group = "onlyoffice";
         isSystemUser = true;
       };
+
+      nginx.extraGroups = [ "onlyoffice" ];
     };
 
     users.groups.onlyoffice = { };
diff --git a/nixos/modules/services/web-apps/openvscode-server.nix b/nixos/modules/services/web-apps/openvscode-server.nix
new file mode 100644
index 00000000000..3daf238c57e
--- /dev/null
+++ b/nixos/modules/services/web-apps/openvscode-server.nix
@@ -0,0 +1,212 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.openvscode-server;
+  defaultUser = "openvscode-server";
+  defaultGroup = defaultUser;
+in
+{
+  options = {
+    services.openvscode-server = {
+      enable = lib.mkEnableOption (lib.mdDoc "openvscode-server");
+
+      package = lib.mkPackageOptionMD pkgs "openvscode-server" { };
+
+      extraPackages = lib.mkOption {
+        default = [ ];
+        description = lib.mdDoc ''
+          Additional packages to add to the openvscode-server {env}`PATH`.
+        '';
+        example = lib.literalExpression "[ pkgs.go ]";
+        type = lib.types.listOf lib.types.package;
+      };
+
+      extraEnvironment = lib.mkOption {
+        type = lib.types.attrsOf lib.types.str;
+        description = lib.mdDoc ''
+          Additional environment variables to pass to openvscode-server.
+        '';
+        default = { };
+        example = { PKG_CONFIG_PATH = "/run/current-system/sw/lib/pkgconfig"; };
+      };
+
+      extraArguments = lib.mkOption {
+        default = [ ];
+        description = lib.mdDoc ''
+          Additional arguments to pass to openvscode-server.
+        '';
+        example = lib.literalExpression ''[ "--log=info" ]'';
+        type = lib.types.listOf lib.types.str;
+      };
+
+      host = lib.mkOption {
+        default = "localhost";
+        description = lib.mdDoc ''
+          The host name or IP address the server should listen to.
+        '';
+        type = lib.types.str;
+      };
+
+      port = lib.mkOption {
+        default = 3000;
+        description = lib.mdDoc ''
+          The port the server should listen to. If 0 is passed a random free port is picked. If a range in the format num-num is passed, a free port from the range (end inclusive) is selected.
+        '';
+        type = lib.types.port;
+      };
+
+      user = lib.mkOption {
+        default = defaultUser;
+        example = "yourUser";
+        description = lib.mdDoc ''
+          The user to run openvscode-server as.
+          By default, a user named `${defaultUser}` will be created.
+        '';
+        type = lib.types.str;
+      };
+
+      group = lib.mkOption {
+        default = defaultGroup;
+        example = "yourGroup";
+        description = lib.mdDoc ''
+          The group to run openvscode-server under.
+          By default, a group named `${defaultGroup}` will be created.
+        '';
+        type = lib.types.str;
+      };
+
+      extraGroups = lib.mkOption {
+        default = [ ];
+        description = lib.mdDoc ''
+          An array of additional groups for the `${defaultUser}` user.
+        '';
+        example = [ "docker" ];
+        type = lib.types.listOf lib.types.str;
+      };
+
+      withoutConnectionToken = lib.mkOption {
+        default = false;
+        description = lib.mdDoc ''
+          Run without a connection token. Only use this if the connection is secured by other means.
+        '';
+        example = true;
+        type = lib.types.bool;
+      };
+
+      socketPath = lib.mkOption {
+        default = null;
+        example = "/run/openvscode/socket";
+        description = lib.mdDoc ''
+          The path to a socket file for the server to listen to.
+        '';
+        type = lib.types.nullOr lib.types.str;
+      };
+
+      userDataDir = lib.mkOption {
+        default = null;
+        description = lib.mdDoc ''
+          Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of Code.
+        '';
+        type = lib.types.nullOr lib.types.str;
+      };
+
+      serverDataDir = lib.mkOption {
+        default = null;
+        description = lib.mdDoc ''
+          Specifies the directory that server data is kept in.
+        '';
+        type = lib.types.nullOr lib.types.str;
+      };
+
+      extensionsDir = lib.mkOption {
+        default = null;
+        description = lib.mdDoc ''
+          Set the root path for extensions.
+        '';
+        type = lib.types.nullOr lib.types.str;
+      };
+
+      telemetryLevel = lib.mkOption {
+        default = null;
+        example = "crash";
+        description = lib.mdDoc ''
+          Sets the initial telemetry level. Valid levels are: 'off', 'crash', 'error' and 'all'.
+        '';
+        type = lib.types.nullOr (lib.types.enum [ "off" "crash" "error" "all" ]);
+      };
+
+      connectionToken = lib.mkOption {
+        default = null;
+        example = "secret-token";
+        description = lib.mdDoc ''
+          A secret that must be included with all requests.
+        '';
+        type = lib.types.nullOr lib.types.str;
+      };
+
+      connectionTokenFile = lib.mkOption {
+        default = null;
+        description = lib.mdDoc ''
+          Path to a file that contains the connection token.
+        '';
+        type = lib.types.nullOr lib.types.str;
+      };
+
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.openvscode-server = {
+      description = "OpenVSCode server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      path = cfg.extraPackages;
+      environment = cfg.extraEnvironment;
+      serviceConfig = {
+        ExecStart = ''
+          ${lib.getExe cfg.package} \
+            --accept-server-license-terms \
+            --host=${cfg.host} \
+            --port=${toString cfg.port} \
+        '' + lib.optionalString (cfg.telemetryLevel != null) ''
+          --telemetry-level=${cfg.telemetryLevel} \
+        '' + lib.optionalString (cfg.withoutConnectionToken) ''
+          --without-connection-token \
+        '' + lib.optionalString (cfg.socketPath != null) ''
+          --socket-path=${cfg.socketPath} \
+        '' + lib.optionalString (cfg.userDataDir != null) ''
+          --user-data-dir=${cfg.userDataDir} \
+        '' + lib.optionalString (cfg.serverDataDir != null) ''
+          --server-data-dir=${cfg.serverDataDir} \
+        '' + lib.optionalString (cfg.extensionsDir != null) ''
+          --extensions-dir=${cfg.extensionsDir} \
+        '' + lib.optionalString (cfg.connectionToken != null) ''
+          --connection-token=${cfg.connectionToken} \
+        '' + lib.optionalString (cfg.connectionTokenFile != null) ''
+          --connection-token-file=${cfg.connectionTokenFile} \
+        '' + lib.escapeShellArgs cfg.extraArguments;
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        RuntimeDirectory = cfg.user;
+        User = cfg.user;
+        Group = cfg.group;
+        Restart = "on-failure";
+      };
+    };
+
+    users.users."${cfg.user}" = lib.mkMerge [
+      (lib.mkIf (cfg.user == defaultUser) {
+        isNormalUser = true;
+        description = "openvscode-server user";
+        inherit (cfg) group;
+      })
+      {
+        packages = cfg.extraPackages;
+        inherit (cfg) extraGroups;
+      }
+    ];
+
+    users.groups."${defaultGroup}" = lib.mkIf (cfg.group == defaultGroup) { };
+  };
+
+  meta.maintainers = [ lib.maintainers.drupol ];
+}
diff --git a/nixos/modules/services/web-apps/outline.nix b/nixos/modules/services/web-apps/outline.nix
index b72dd8243bb..1d8298963e6 100644
--- a/nixos/modules/services/web-apps/outline.nix
+++ b/nixos/modules/services/web-apps/outline.nix
@@ -3,8 +3,12 @@
 let
   defaultUser = "outline";
   cfg = config.services.outline;
+  inherit (lib) mkRemovedOptionModule;
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "outline" "sequelizeArguments" ] "Database migration are run agains configurated database by outline directly")
+  ];
   # See here for a reference of all the options:
   #   https://github.com/outline/outline/blob/v0.67.0/.env.sample
   #   https://github.com/outline/outline/blob/v0.67.0/app.json
@@ -25,7 +29,7 @@ in
           # to still land in the same team. Note that this effectively makes
           # Outline a single-team instance.
           patchPhase = ${"''"}
-            sed -i 's/const domain = parts\.length && parts\[1\];/const domain = "example.com";/g' server/routes/auth/providers/oidc.ts
+            sed -i 's/const domain = parts\.length && parts\[1\];/const domain = "example.com";/g' plugins/oidc/server/auth/oidc.ts
           ${"''"};
         })
       '';
@@ -51,15 +55,6 @@ in
       '';
     };
 
-    sequelizeArguments = lib.mkOption {
-      type = lib.types.str;
-      default = "";
-      example = "--env=production-ssl-disabled";
-      description = lib.mdDoc ''
-        Optional arguments to pass to `sequelize` calls.
-      '';
-    };
-
     #
     # Required options
     #
@@ -583,16 +578,6 @@ in
     systemd.services.outline = let
       localRedisUrl = "redis+unix:///run/redis-outline/redis.sock";
       localPostgresqlUrl = "postgres://localhost/outline?host=/run/postgresql";
-
-      # Create an outline-sequalize wrapper (a wrapper around the wrapper) that
-      # has the config file's path baked in. This is necessary because there is
-      # at least one occurrence of outline calling this from its own code.
-      sequelize = pkgs.writeShellScriptBin "outline-sequelize" ''
-        exec ${cfg.package}/bin/outline-sequelize \
-          --config $RUNTIME_DIRECTORY/database.json \
-          ${cfg.sequelizeArguments} \
-          "$@"
-      '';
     in {
       description = "Outline wiki and knowledge base";
       wantedBy = [ "multi-user.target" ];
@@ -603,7 +588,6 @@ in
         ++ lib.optional (cfg.redisUrl == "local") "redis-outline.service";
       path = [
         pkgs.openssl # Required by the preStart script
-        sequelize
       ];
 
 
@@ -687,44 +671,6 @@ in
           openssl rand -hex 32 > ${lib.escapeShellArg cfg.utilsSecretFile}
         fi
 
-        # The config file is required for the CLI, the DATABASE_URL environment
-        # variable is read by the app.
-        ${if (cfg.databaseUrl == "local") then ''
-          cat <<EOF > $RUNTIME_DIRECTORY/database.json
-          {
-            "production": {
-              "dialect": "postgres",
-              "host": "/run/postgresql",
-              "username": null,
-              "password": null
-            }
-          }
-          EOF
-          export DATABASE_URL=${lib.escapeShellArg localPostgresqlUrl}
-          export PGSSLMODE=disable
-        '' else ''
-          cat <<EOF > $RUNTIME_DIRECTORY/database.json
-          {
-            "production": {
-              "use_env_variable": "DATABASE_URL",
-              "dialect": "postgres",
-              "dialectOptions": {
-                "ssl": {
-                  "rejectUnauthorized": false
-                }
-              }
-            },
-            "production-ssl-disabled": {
-              "use_env_variable": "DATABASE_URL",
-              "dialect": "postgres"
-            }
-          }
-          EOF
-          export DATABASE_URL=${lib.escapeShellArg cfg.databaseUrl}
-        ''}
-
-        cd $RUNTIME_DIRECTORY
-        ${sequelize}/bin/outline-sequelize db:migrate
       '';
 
       script = ''
@@ -781,7 +727,7 @@ in
         RuntimeDirectoryMode = "0750";
         # This working directory is required to find stuff like the set of
         # onboarding files:
-        WorkingDirectory = "${cfg.package}/share/outline/build";
+        WorkingDirectory = "${cfg.package}/share/outline";
       };
     };
   };
diff --git a/nixos/modules/services/web-apps/peertube.nix b/nixos/modules/services/web-apps/peertube.nix
index 7e418f2869c..4826b2cab6a 100644
--- a/nixos/modules/services/web-apps/peertube.nix
+++ b/nixos/modules/services/web-apps/peertube.nix
@@ -52,9 +52,7 @@ let
 
   envFile = pkgs.writeText "peertube.env" (lib.concatMapStrings (s: s + "\n") (
     (lib.concatLists (lib.mapAttrsToList (name: value:
-      if value != null then [
-        "${name}=\"${toString value}\""
-      ] else []
+      lib.optional (value != null) ''${name}="${toString value}"''
     ) env))));
 
   peertubeEnv = pkgs.writeShellScriptBin "peertube-env" ''
@@ -429,7 +427,7 @@ in {
 
       environment = env;
 
-      path = with pkgs; [ bashInteractive ffmpeg nodejs-16_x openssl yarn python3 ];
+      path = with pkgs; [ bashInteractive ffmpeg nodejs_18 openssl yarn python3 ];
 
       script = ''
         #!/bin/sh
@@ -490,7 +488,7 @@ in {
     services.nginx = lib.mkIf cfg.configureNginx {
       enable = true;
       virtualHosts."${cfg.localDomain}" = {
-        root = "/var/lib/peertube";
+        root = "/var/lib/peertube/www";
 
         # Application
         locations."/" = {
@@ -593,7 +591,7 @@ in {
 
         # 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";
+          tryFiles = "/client-overrides/$1 /client/$1 $1";
           priority = 1310;
         };
 
@@ -859,7 +857,7 @@ in {
           home = cfg.package;
         };
       })
-      (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package peertubeEnv peertubeCli pkgs.ffmpeg pkgs.nodejs-16_x pkgs.yarn ])
+      (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package peertubeEnv peertubeCli pkgs.ffmpeg pkgs.nodejs_18 pkgs.yarn ])
       (lib.mkIf cfg.redis.enableUnixSocket {${config.services.peertube.user}.extraGroups = [ "redis-peertube" ];})
     ];
 
diff --git a/nixos/modules/services/web-apps/photoprism.nix b/nixos/modules/services/web-apps/photoprism.nix
new file mode 100644
index 00000000000..d5ca6014780
--- /dev/null
+++ b/nixos/modules/services/web-apps/photoprism.nix
@@ -0,0 +1,155 @@
+{ config, pkgs, lib, ... }:
+let
+  cfg = config.services.photoprism;
+
+  env = {
+    PHOTOPRISM_ORIGINALS_PATH = cfg.originalsPath;
+    PHOTOPRISM_STORAGE_PATH = cfg.storagePath;
+    PHOTOPRISM_IMPORT_PATH = cfg.importPath;
+    PHOTOPRISM_HTTP_HOST = cfg.address;
+    PHOTOPRISM_HTTP_PORT = toString cfg.port;
+  } // (
+    lib.mapAttrs (_: toString) cfg.settings
+  );
+
+  manage =
+    let
+      setupEnv = lib.concatStringsSep "\n" (lib.mapAttrsToList (name: val: "export ${name}=${lib.escapeShellArg val}") env);
+    in
+    pkgs.writeShellScript "manage" ''
+      ${setupEnv}
+      exec ${cfg.package}/bin/photoprism "$@"
+    '';
+in
+{
+  meta.maintainers = with lib.maintainers; [ stunkymonkey ];
+
+  options.services.photoprism = {
+
+    enable = lib.mkEnableOption (lib.mdDoc "Photoprism web server");
+
+    passwordFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = lib.mdDoc ''
+        Admin password file.
+      '';
+    };
+
+    address = lib.mkOption {
+      type = lib.types.str;
+      default = "localhost";
+      description = lib.mdDoc ''
+        Web interface address.
+      '';
+    };
+
+    port = lib.mkOption {
+      type = lib.types.port;
+      default = 2342;
+      description = lib.mdDoc ''
+        Web interface port.
+      '';
+    };
+
+    originalsPath = lib.mkOption {
+      type = lib.types.path;
+      default = null;
+      example = "/data/photos";
+      description = lib.mdDoc ''
+        Storage path of your original media files (photos and videos).
+      '';
+    };
+
+    importPath = lib.mkOption {
+      type = lib.types.str;
+      default = "import";
+      description = lib.mdDoc ''
+        Relative or absolute to the `originalsPath` from where the files should be imported.
+      '';
+    };
+
+    storagePath = lib.mkOption {
+      type = lib.types.path;
+      default = "/var/lib/photoprism";
+      description = lib.mdDoc ''
+        Location for sidecar, cache, and database files.
+      '';
+    };
+
+    package = lib.mkPackageOptionMD pkgs "photoprism" { };
+
+    settings = lib.mkOption {
+      type = lib.types.attrsOf lib.types.str;
+      default = { };
+      description = lib.mdDoc ''
+        See [the getting-started guide](https://docs.photoprism.app/getting-started/config-options/) for available options.
+      '';
+      example = {
+        PHOTOPRISM_DEFAULT_LOCALE = "de";
+        PHOTOPRISM_ADMIN_USER = "root";
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.photoprism = {
+      description = "Photoprism server";
+
+      serviceConfig = {
+        Restart = "on-failure";
+        User = "photoprism";
+        Group = "photoprism";
+        DynamicUser = true;
+        StateDirectory = "photoprism";
+        WorkingDirectory = "/var/lib/photoprism";
+        RuntimeDirectory = "photoprism";
+
+        LoadCredential = lib.optionalString (cfg.passwordFile != null)
+          "PHOTOPRISM_ADMIN_PASSWORD:${cfg.passwordFile}";
+
+        CapabilityBoundingSet = "";
+        LockPersonality = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ];
+        UMask = "0066";
+      } // lib.optionalAttrs (cfg.port < 1024) {
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+      };
+
+      wantedBy = [ "multi-user.target" ];
+      environment = env;
+
+      # reminder: easier password configuration will come in https://github.com/photoprism/photoprism/pull/2302
+      preStart = ''
+        ln -sf ${manage} photoprism-manage
+
+        ${lib.optionalString (cfg.passwordFile != null) ''
+          export PHOTOPRISM_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/PHOTOPRISM_ADMIN_PASSWORD")
+        ''}
+        exec ${cfg.package}/bin/photoprism migrations run -f
+      '';
+
+      script = ''
+        ${lib.optionalString (cfg.passwordFile != null) ''
+          export PHOTOPRISM_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/PHOTOPRISM_ADMIN_PASSWORD")
+        ''}
+        exec ${cfg.package}/bin/photoprism start
+      '';
+    };
+  };
+}
+
diff --git a/nixos/modules/services/web-apps/pict-rs.md b/nixos/modules/services/web-apps/pict-rs.md
index 4b622049909..2fa6bb3aebc 100644
--- a/nixos/modules/services/web-apps/pict-rs.md
+++ b/nixos/modules/services/web-apps/pict-rs.md
@@ -15,6 +15,7 @@ this will start the http server on port 8080 by default.
 ## Usage {#module-services-pict-rs-usage}
 
 pict-rs offers the following endpoints:
+
 - `POST /image` for uploading an image. Uploaded content must be valid multipart/form-data with an
     image array located within the `images[]` key
 
diff --git a/nixos/modules/services/web-apps/pict-rs.nix b/nixos/modules/services/web-apps/pict-rs.nix
index ee9ff9b484f..3270715a051 100644
--- a/nixos/modules/services/web-apps/pict-rs.nix
+++ b/nixos/modules/services/web-apps/pict-rs.nix
@@ -5,9 +5,7 @@ let
 in
 {
   meta.maintainers = with maintainers; [ happysalada ];
-  # Don't edit the docbook xml directly, edit the md and generate it:
-  # `pandoc pict-rs.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > pict-rs.xml`
-  meta.doc = ./pict-rs.xml;
+  meta.doc = ./pict-rs.md;
 
   options.services.pict-rs = {
     enable = mkEnableOption (lib.mdDoc "pict-rs server");
@@ -36,8 +34,8 @@ in
   config = lib.mkIf cfg.enable {
     systemd.services.pict-rs = {
       environment = {
-        PICTRS_PATH = cfg.dataDir;
-        PICTRS_ADDR = "${cfg.address}:${toString cfg.port}";
+        PICTRS__PATH = cfg.dataDir;
+        PICTRS__ADDR = "${cfg.address}:${toString cfg.port}";
       };
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
diff --git a/nixos/modules/services/web-apps/pict-rs.xml b/nixos/modules/services/web-apps/pict-rs.xml
deleted file mode 100644
index bf129f5cc2a..00000000000
--- a/nixos/modules/services/web-apps/pict-rs.xml
+++ /dev/null
@@ -1,162 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-pict-rs">
-  <title>Pict-rs</title>
-  <para>
-    pict-rs is a a simple image hosting service.
-  </para>
-  <section xml:id="module-services-pict-rs-quickstart">
-    <title>Quickstart</title>
-    <para>
-      the minimum to start pict-rs is
-    </para>
-    <programlisting language="bash">
-services.pict-rs.enable = true;
-</programlisting>
-    <para>
-      this will start the http server on port 8080 by default.
-    </para>
-  </section>
-  <section xml:id="module-services-pict-rs-usage">
-    <title>Usage</title>
-    <para>
-      pict-rs offers the following endpoints: -
-      <literal>POST /image</literal> for uploading an image. Uploaded
-      content must be valid multipart/form-data with an image array
-      located within the <literal>images[]</literal> key
-    </para>
-    <programlisting>
-This endpoint returns the following JSON structure on success with a 201 Created status
-```json
-{
-    &quot;files&quot;: [
-        {
-            &quot;delete_token&quot;: &quot;JFvFhqJA98&quot;,
-            &quot;file&quot;: &quot;lkWZDRvugm.jpg&quot;
-        },
-        {
-            &quot;delete_token&quot;: &quot;kAYy9nk2WK&quot;,
-            &quot;file&quot;: &quot;8qFS0QooAn.jpg&quot;
-        },
-        {
-            &quot;delete_token&quot;: &quot;OxRpM3sf0Y&quot;,
-            &quot;file&quot;: &quot;1hJaYfGE01.jpg&quot;
-        }
-    ],
-    &quot;msg&quot;: &quot;ok&quot;
-}
-```
-</programlisting>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>GET /image/download?url=...</literal> Download an
-          image from a remote server, returning the same JSON payload as
-          the <literal>POST</literal> endpoint
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>GET /image/original/{file}</literal> for getting a
-          full-resolution image. <literal>file</literal> here is the
-          <literal>file</literal> key from the <literal>/image</literal>
-          endpoint’s JSON
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>GET /image/details/original/{file}</literal> for
-          getting the details of a full-resolution image. The returned
-          JSON is structured like so:
-          <literal>json     {         &quot;width&quot;: 800,         &quot;height&quot;: 537,         &quot;content_type&quot;: &quot;image/webp&quot;,         &quot;created_at&quot;: [             2020,             345,             67376,             394363487         ]     }</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>GET /image/process.{ext}?src={file}&amp;...</literal>
-          get a file with transformations applied. existing
-          transformations include
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              <literal>identity=true</literal>: apply no changes
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>blur={float}</literal>: apply a gaussian blur to
-              the file
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>thumbnail={int}</literal>: produce a thumbnail of
-              the image fitting inside an <literal>{int}</literal> by
-              <literal>{int}</literal> square using raw pixel sampling
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>resize={int}</literal>: produce a thumbnail of
-              the image fitting inside an <literal>{int}</literal> by
-              <literal>{int}</literal> square using a Lanczos2 filter.
-              This is slower than sampling but looks a bit better in
-              some cases
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>crop={int-w}x{int-h}</literal>: produce a cropped
-              version of the image with an <literal>{int-w}</literal> by
-              <literal>{int-h}</literal> aspect ratio. The resulting
-              crop will be centered on the image. Either the width or
-              height of the image will remain full-size, depending on
-              the image’s aspect ratio and the requested aspect ratio.
-              For example, a 1600x900 image cropped with a 1x1 aspect
-              ratio will become 900x900. A 1600x1100 image cropped with
-              a 16x9 aspect ratio will become 1600x900.
-            </para>
-          </listitem>
-        </itemizedlist>
-        <para>
-          Supported <literal>ext</literal> file extensions include
-          <literal>png</literal>, <literal>jpg</literal>, and
-          <literal>webp</literal>
-        </para>
-        <para>
-          An example of usage could be
-          <literal>GET /image/process.jpg?src=asdf.png&amp;thumbnail=256&amp;blur=3.0</literal>
-          which would create a 256x256px JPEG thumbnail and blur it
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>GET /image/details/process.{ext}?src={file}&amp;...</literal>
-          for getting the details of a processed image. The returned
-          JSON is the same format as listed for the full-resolution
-          details endpoint.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>DELETE /image/delete/{delete_token}/{file}</literal>
-          or <literal>GET /image/delete/{delete_token}/{file}</literal>
-          to delete a file, where <literal>delete_token</literal> and
-          <literal>file</literal> are from the <literal>/image</literal>
-          endpoint’s JSON
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="module-services-pict-rs-missing">
-    <title>Missing</title>
-    <itemizedlist spacing="compact">
-      <listitem>
-        <para>
-          Configuring the secure-api-key is not included yet. The
-          envisioned basic use case is consumption on localhost by other
-          services without exposing the service to the internet.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/pixelfed.nix b/nixos/modules/services/web-apps/pixelfed.nix
new file mode 100644
index 00000000000..159fb52476a
--- /dev/null
+++ b/nixos/modules/services/web-apps/pixelfed.nix
@@ -0,0 +1,483 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.pixelfed;
+  user = cfg.user;
+  group = cfg.group;
+  pixelfed = cfg.package.override { inherit (cfg) dataDir runtimeDir; };
+  # https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L185-L190
+  extraPrograms = with pkgs; [ jpegoptim optipng pngquant gifsicle ffmpeg ];
+  # Ensure PHP extensions: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L135-L147
+  phpPackage = cfg.phpPackage.buildEnv {
+    extensions = { enabled, all }:
+      enabled
+      ++ (with all; [ bcmath ctype curl mbstring gd intl zip redis imagick ]);
+  };
+  configFile =
+    pkgs.writeText "pixelfed-env" (lib.generators.toKeyValue { } cfg.settings);
+  # Management script
+  pixelfed-manage = pkgs.writeShellScriptBin "pixelfed-manage" ''
+    cd ${pixelfed}
+    sudo=exec
+    if [[ "$USER" != ${user} ]]; then
+      sudo='exec /run/wrappers/bin/sudo -u ${user}'
+    fi
+    $sudo ${phpPackage}/bin/php artisan "$@"
+  '';
+  dbSocket = {
+    "pgsql" = "/run/postgresql";
+    "mysql" = "/run/mysqld/mysqld.sock";
+  }.${cfg.database.type};
+  dbService = {
+    "pgsql" = "postgresql.service";
+    "mysql" = "mysql.service";
+  }.${cfg.database.type};
+  redisService = "redis-pixelfed.service";
+in {
+  options.services = {
+    pixelfed = {
+      enable = mkEnableOption (lib.mdDoc "a Pixelfed instance");
+      package = mkPackageOptionMD pkgs "pixelfed" { };
+      phpPackage = mkPackageOptionMD pkgs "php81" { };
+
+      user = mkOption {
+        type = types.str;
+        default = "pixelfed";
+        description = lib.mdDoc ''
+          User account under which pixelfed runs.
+
+          ::: {.note}
+          If left as the default value this user will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the user exists before the pixelfed application starts.
+          :::
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "pixelfed";
+        description = lib.mdDoc ''
+          Group account under which pixelfed runs.
+
+          ::: {.note}
+          If left as the default value this group will automatically be created
+          on system activation, otherwise you are responsible for
+          ensuring the group exists before the pixelfed application starts.
+          :::
+        '';
+      };
+
+      domain = mkOption {
+        type = types.str;
+        description = lib.mdDoc ''
+          FQDN for the Pixelfed instance.
+        '';
+      };
+
+      secretFile = mkOption {
+        type = types.path;
+        description = lib.mdDoc ''
+          A secret file to be sourced for the .env settings.
+          Place `APP_KEY` and other settings that should not end up in the Nix store here.
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; (attrsOf (oneOf [ bool int str ]));
+        description = lib.mdDoc ''
+          .env settings for Pixelfed.
+          Secrets should use `secretFile` option instead.
+        '';
+      };
+
+      nginx = mkOption {
+        type = types.nullOr (types.submodule
+          (import ../web-servers/nginx/vhost-options.nix {
+            inherit config lib;
+          }));
+        default = null;
+        example = lib.literalExpression ''
+          {
+            serverAliases = [
+              "pics.''${config.networking.domain}"
+            ];
+            enableACME = true;
+            forceHttps = true;
+          }
+        '';
+        description = lib.mdDoc ''
+          With this option, you can customize an nginx virtual host which already has sensible defaults for Dolibarr.
+          Set to {} if you do not need any customization to the virtual host.
+          If enabled, then by default, the {option}`serverName` is
+          `''${domain}`,
+          If this is set to null (the default), no nginx virtualHost will be configured.
+        '';
+      };
+
+      redis.createLocally = mkEnableOption
+        (lib.mdDoc "a local Redis database using UNIX socket authentication")
+        // {
+          default = true;
+        };
+
+      database = {
+        createLocally = mkEnableOption
+          (lib.mdDoc "a local database using UNIX socket authentication") // {
+            default = true;
+          };
+        automaticMigrations = mkEnableOption
+          (lib.mdDoc "automatic migrations for database schema and data") // {
+            default = true;
+          };
+
+        type = mkOption {
+          type = types.enum [ "mysql" "pgsql" ];
+          example = "pgsql";
+          default = "mysql";
+          description = lib.mdDoc ''
+            Database engine to use.
+            Note that PGSQL is not well supported: https://github.com/pixelfed/pixelfed/issues/2727
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "pixelfed";
+          description = lib.mdDoc "Database name.";
+        };
+      };
+
+      maxUploadSize = mkOption {
+        type = types.str;
+        default = "8M";
+        description = lib.mdDoc ''
+          Max upload size with units.
+        '';
+      };
+
+      poolConfig = mkOption {
+        type = with types; attrsOf (oneOf [ int str bool ]);
+        default = { };
+
+        description = lib.mdDoc ''
+          Options for Pixelfed's PHP-FPM pool.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/pixelfed";
+        description = lib.mdDoc ''
+          State directory of the `pixelfed` user which holds
+          the application's state and data.
+        '';
+      };
+
+      runtimeDir = mkOption {
+        type = types.str;
+        default = "/run/pixelfed";
+        description = lib.mdDoc ''
+          Ruutime directory of the `pixelfed` user which holds
+          the application's caches and temporary files.
+        '';
+      };
+
+      schedulerInterval = mkOption {
+        type = types.str;
+        default = "1d";
+        description = lib.mdDoc "How often the Pixelfed cron task should run";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.pixelfed = mkIf (cfg.user == "pixelfed") {
+      isSystemUser = true;
+      group = cfg.group;
+      extraGroups = lib.optional cfg.redis.createLocally "redis-pixelfed";
+    };
+    users.groups.pixelfed = mkIf (cfg.group == "pixelfed") { };
+
+    services.redis.servers.pixelfed.enable = lib.mkIf cfg.redis.createLocally true;
+    services.pixelfed.settings = mkMerge [
+      ({
+        APP_ENV = mkDefault "production";
+        APP_DEBUG = mkDefault false;
+        # https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L312-L316
+        APP_URL = mkDefault "https://${cfg.domain}";
+        ADMIN_DOMAIN = mkDefault cfg.domain;
+        APP_DOMAIN = mkDefault cfg.domain;
+        SESSION_DOMAIN = mkDefault cfg.domain;
+        SESSION_SECURE_COOKIE = mkDefault true;
+        OPEN_REGISTRATION = mkDefault false;
+        # ActivityPub: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L360-L364
+        ACTIVITY_PUB = mkDefault true;
+        AP_REMOTE_FOLLOW = mkDefault true;
+        AP_INBOX = mkDefault true;
+        AP_OUTBOX = mkDefault true;
+        AP_SHAREDINBOX = mkDefault true;
+        # Image optimization: https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L367-L404
+        PF_OPTIMIZE_IMAGES = mkDefault true;
+        IMAGE_DRIVER = mkDefault "imagick";
+        # Mobile APIs
+        OAUTH_ENABLED = mkDefault true;
+        # https://github.com/pixelfed/pixelfed/blob/dev/app/Console/Commands/Installer.php#L351
+        EXP_EMC = mkDefault true;
+        # Defer to systemd
+        LOG_CHANNEL = mkDefault "stderr";
+        # TODO: find out the correct syntax?
+        # TRUST_PROXIES = mkDefault "127.0.0.1/8, ::1/128";
+      })
+      (mkIf (cfg.redis.createLocally) {
+        BROADCAST_DRIVER = mkDefault "redis";
+        CACHE_DRIVER = mkDefault "redis";
+        QUEUE_DRIVER = mkDefault "redis";
+        SESSION_DRIVER = mkDefault "redis";
+        WEBSOCKET_REPLICATION_MODE = mkDefault "redis";
+        # Support phpredis and predis configuration-style.
+        REDIS_SCHEME = "unix";
+        REDIS_HOST = config.services.redis.servers.pixelfed.unixSocket;
+        REDIS_PATH = config.services.redis.servers.pixelfed.unixSocket;
+      })
+      (mkIf (cfg.database.createLocally) {
+        DB_CONNECTION = cfg.database.type;
+        DB_SOCKET = dbSocket;
+        DB_DATABASE = cfg.database.name;
+        DB_USERNAME = user;
+        # No TCP/IP connection.
+        DB_PORT = 0;
+      })
+    ];
+
+    environment.systemPackages = [ pixelfed-manage ];
+
+    services.mysql =
+      mkIf (cfg.database.createLocally && cfg.database.type == "mysql") {
+        enable = mkDefault true;
+        package = mkDefault pkgs.mariadb;
+        ensureDatabases = [ cfg.database.name ];
+        ensureUsers = [{
+          name = user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }];
+      };
+
+    services.postgresql =
+      mkIf (cfg.database.createLocally && cfg.database.type == "pgsql") {
+        enable = mkDefault true;
+        ensureDatabases = [ cfg.database.name ];
+        ensureUsers = [{
+          name = user;
+          ensurePermissions = { };
+        }];
+      };
+
+    # Make each individual option overridable with lib.mkDefault.
+    services.pixelfed.poolConfig = lib.mapAttrs' (n: v: lib.nameValuePair n (lib.mkDefault v)) {
+      "pm" = "dynamic";
+      "php_admin_value[error_log]" = "stderr";
+      "php_admin_flag[log_errors]" = true;
+      "catch_workers_output" = true;
+      "pm.max_children" = "32";
+      "pm.start_servers" = "2";
+      "pm.min_spare_servers" = "2";
+      "pm.max_spare_servers" = "4";
+      "pm.max_requests" = "500";
+    };
+
+    services.phpfpm.pools.pixelfed = {
+      inherit user group;
+      inherit phpPackage;
+
+      phpOptions = ''
+        post_max_size = ${toString cfg.maxUploadSize}
+        upload_max_filesize = ${toString cfg.maxUploadSize}
+        max_execution_time = 600;
+      '';
+
+      settings = {
+        "listen.owner" = user;
+        "listen.group" = group;
+        "listen.mode" = "0660";
+        "catch_workers_output" = "yes";
+      } // cfg.poolConfig;
+    };
+
+    systemd.services.phpfpm-pixelfed.after = [ "pixelfed-data-setup.service" ];
+    systemd.services.phpfpm-pixelfed.requires =
+      [ "pixelfed-horizon.service" "pixelfed-data-setup.service" ]
+      ++ lib.optional cfg.database.createLocally dbService
+      ++ lib.optional cfg.redis.createLocally redisService;
+    # Ensure image optimizations programs are available.
+    systemd.services.phpfpm-pixelfed.path = extraPrograms;
+
+    systemd.services.pixelfed-horizon = {
+      description = "Pixelfed task queueing via Laravel Horizon framework";
+      after = [ "network.target" "pixelfed-data-setup.service" ];
+      requires = [ "pixelfed-data-setup.service" ]
+        ++ (lib.optional cfg.database.createLocally dbService)
+        ++ (lib.optional cfg.redis.createLocally redisService);
+      wantedBy = [ "multi-user.target" ];
+      # Ensure image optimizations programs are available.
+      path = extraPrograms;
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pixelfed-manage}/bin/pixelfed-manage horizon";
+        StateDirectory =
+          lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
+        User = user;
+        Group = group;
+        Restart = "on-failure";
+      };
+    };
+
+    systemd.timers.pixelfed-cron = {
+      description = "Pixelfed periodic tasks timer";
+      after = [ "pixelfed-data-setup.service" ];
+      requires = [ "phpfpm-pixelfed.service" ];
+      wantedBy = [ "timers.target" ];
+
+      timerConfig = {
+        OnBootSec = cfg.schedulerInterval;
+        OnUnitActiveSec = cfg.schedulerInterval;
+      };
+    };
+
+    systemd.services.pixelfed-cron = {
+      description = "Pixelfed periodic tasks";
+      # Ensure image optimizations programs are available.
+      path = extraPrograms;
+
+      serviceConfig = {
+        ExecStart = "${pixelfed-manage}/bin/pixelfed-manage schedule:run";
+        User = user;
+        Group = group;
+        StateDirectory =
+          lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
+      };
+    };
+
+    systemd.services.pixelfed-data-setup = {
+      description =
+        "Pixelfed setup: migrations, environment file update, cache reload, data changes";
+      wantedBy = [ "multi-user.target" ];
+      after = lib.optional cfg.database.createLocally dbService;
+      requires = lib.optional cfg.database.createLocally dbService;
+      path = with pkgs; [ bash pixelfed-manage rsync ] ++ extraPrograms;
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = user;
+        Group = group;
+        StateDirectory =
+          lib.mkIf (cfg.dataDir == "/var/lib/pixelfed") "pixelfed";
+        LoadCredential = "env-secrets:${cfg.secretFile}";
+        UMask = "077";
+      };
+
+      script = ''
+        # Before running any PHP program, cleanup the code cache.
+        # It's necessary if you upgrade the application otherwise you might
+        # try to import non-existent modules.
+        rm -f ${cfg.runtimeDir}/app.php
+        rm -rf ${cfg.runtimeDir}/cache/*
+
+        # Concatenate non-secret .env and secret .env
+        rm -f ${cfg.dataDir}/.env
+        cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env
+        echo -e '\n' >> ${cfg.dataDir}/.env
+        cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env
+
+        # Link the static storage (package provided) to the runtime storage
+        # Necessary for cities.json and static images.
+        mkdir -p ${cfg.dataDir}/storage
+        rsync -av --no-perms ${pixelfed}/storage-static/ ${cfg.dataDir}/storage
+        chmod -R +w ${cfg.dataDir}/storage
+
+        chmod g+x ${cfg.dataDir}/storage ${cfg.dataDir}/storage/app
+        chmod -R g+rX ${cfg.dataDir}/storage/app/public
+
+        # Link the app.php in the runtime folder.
+        # We cannot link the cache folder only because bootstrap folder needs to be writeable.
+        ln -sf ${pixelfed}/bootstrap-static/app.php ${cfg.runtimeDir}/app.php
+
+        # https://laravel.com/docs/10.x/filesystem#the-public-disk
+        # Creating the public/storage → storage/app/public link
+        # is unnecessary as it's part of the installPhase of pixelfed.
+
+        # Install Horizon
+        # FIXME: require write access to public/ — should be done as part of install — pixelfed-manage horizon:publish
+
+        # Perform the first migration.
+        [[ ! -f ${cfg.dataDir}/.initial-migration ]] && pixelfed-manage migrate --force && touch ${cfg.dataDir}/.initial-migration
+
+        ${lib.optionalString cfg.database.automaticMigrations ''
+          # Force migrate the database.
+          pixelfed-manage migrate --force
+        ''}
+
+        # Import location data
+        pixelfed-manage import:cities
+
+        ${lib.optionalString cfg.settings.ACTIVITY_PUB ''
+          # ActivityPub federation bookkeeping
+          [[ ! -f ${cfg.dataDir}/.instance-actor-created ]] && pixelfed-manage instance:actor && touch ${cfg.dataDir}/.instance-actor-created
+        ''}
+
+        ${lib.optionalString cfg.settings.OAUTH_ENABLED ''
+          # Generate Passport encryption keys
+          [[ ! -f ${cfg.dataDir}/.passport-keys-generated ]] && pixelfed-manage passport:keys && touch ${cfg.dataDir}/.passport-keys-generated
+        ''}
+
+        pixelfed-manage route:cache
+        pixelfed-manage view:cache
+        pixelfed-manage config:cache
+      '';
+    };
+
+    systemd.tmpfiles.rules = [
+      # Cache must live across multiple systemd units runtimes.
+      "d ${cfg.runtimeDir}/                         0700 ${user} ${group} - -"
+      "d ${cfg.runtimeDir}/cache                    0700 ${user} ${group} - -"
+    ];
+
+    # Enable NGINX to access our phpfpm-socket.
+    users.users."${config.services.nginx.user}".extraGroups = [ cfg.group ];
+    services.nginx = mkIf (cfg.nginx != null) {
+      enable = true;
+      virtualHosts."${cfg.domain}" = mkMerge [
+        cfg.nginx
+        {
+          root = lib.mkForce "${pixelfed}/public/";
+          locations."/".tryFiles = "$uri $uri/ /index.php?$query_string";
+          locations."/favicon.ico".extraConfig = ''
+            access_log off; log_not_found off;
+          '';
+          locations."/robots.txt".extraConfig = ''
+            access_log off; log_not_found off;
+          '';
+          locations."~ \\.php$".extraConfig = ''
+            fastcgi_split_path_info ^(.+\.php)(/.+)$;
+            fastcgi_pass unix:${config.services.phpfpm.pools.pixelfed.socket};
+            fastcgi_index index.php;
+          '';
+          locations."~ /\\.(?!well-known).*".extraConfig = ''
+            deny all;
+          '';
+          extraConfig = ''
+            add_header X-Frame-Options "SAMEORIGIN";
+            add_header X-XSS-Protection "1; mode=block";
+            add_header X-Content-Type-Options "nosniff";
+            index index.html index.htm index.php;
+            error_page 404 /index.php;
+            client_max_body_size ${toString cfg.maxUploadSize};
+          '';
+        }
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/plausible.md b/nixos/modules/services/web-apps/plausible.md
new file mode 100644
index 00000000000..1328ce69441
--- /dev/null
+++ b/nixos/modules/services/web-apps/plausible.md
@@ -0,0 +1,35 @@
+# Plausible {#module-services-plausible}
+
+[Plausible](https://plausible.io/) is a privacy-friendly alternative to
+Google analytics.
+
+## Basic Usage {#module-services-plausible-basic-usage}
+
+At first, a secret key is needed to be generated. This can be done with e.g.
+```ShellSession
+$ openssl rand -base64 64
+```
+
+After that, `plausible` can be deployed like this:
+```
+{
+  services.plausible = {
+    enable = true;
+    adminUser = {
+      # activate is used to skip the email verification of the admin-user that's
+      # automatically created by plausible. This is only supported if
+      # postgresql is configured by the module. This is done by default, but
+      # can be turned off with services.plausible.database.postgres.setup.
+      activate = true;
+      email = "admin@localhost";
+      passwordFile = "/run/secrets/plausible-admin-pwd";
+    };
+    server = {
+      baseUrl = "http://analytics.example.org";
+      # secretKeybaseFile is a path to the file which contains the secret generated
+      # with openssl as described above.
+      secretKeybaseFile = "/run/secrets/plausible-secret-key-base";
+    };
+  };
+}
+```
diff --git a/nixos/modules/services/web-apps/plausible.nix b/nixos/modules/services/web-apps/plausible.nix
index e5dc1b10360..911daa53e65 100644
--- a/nixos/modules/services/web-apps/plausible.nix
+++ b/nixos/modules/services/web-apps/plausible.nix
@@ -9,6 +9,8 @@ in {
   options.services.plausible = {
     enable = mkEnableOption (lib.mdDoc "plausible");
 
+    package = mkPackageOptionMD pkgs "plausible" { };
+
     releaseCookiePath = mkOption {
       type = with types; either str path;
       description = lib.mdDoc ''
@@ -180,12 +182,12 @@ in {
 
     services.epmd.enable = true;
 
-    environment.systemPackages = [ pkgs.plausible ];
+    environment.systemPackages = [ cfg.package ];
 
     systemd.services = mkMerge [
       {
         plausible = {
-          inherit (pkgs.plausible.meta) description;
+          inherit (cfg.package.meta) description;
           documentation = [ "https://plausible.io/docs/self-hosting" ];
           wantedBy = [ "multi-user.target" ];
           after = optional cfg.database.clickhouse.setup "clickhouse.service"
@@ -233,18 +235,21 @@ in {
             SMTP_USER_NAME = cfg.mail.smtp.user;
           });
 
-          path = [ pkgs.plausible ]
+          path = [ cfg.package ]
             ++ optional cfg.database.postgres.setup config.services.postgresql.package;
           script = ''
-            export CONFIG_DIR=$CREDENTIALS_DIRECTORY
-
             export RELEASE_COOKIE="$(< $CREDENTIALS_DIRECTORY/RELEASE_COOKIE )"
+            export ADMIN_USER_PWD="$(< $CREDENTIALS_DIRECTORY/ADMIN_USER_PWD )"
+            export SECRET_KEY_BASE="$(< $CREDENTIALS_DIRECTORY/SECRET_KEY_BASE )"
+
+            ${lib.optionalString (cfg.mail.smtp.passwordFile != null)
+              ''export SMTP_USER_PWD="$(< $CREDENTIALS_DIRECTORY/SMTP_USER_PWD )"''}
 
             # setup
-            ${pkgs.plausible}/createdb.sh
-            ${pkgs.plausible}/migrate.sh
+            ${cfg.package}/createdb.sh
+            ${cfg.package}/migrate.sh
             ${optionalString cfg.adminUser.activate ''
-              if ! ${pkgs.plausible}/init-admin.sh | grep 'already exists'; then
+              if ! ${cfg.package}/init-admin.sh | grep 'already exists'; then
                 psql -d plausible <<< "UPDATE users SET email_verified=true;"
               fi
             ''}
@@ -292,5 +297,5 @@ in {
   };
 
   meta.maintainers = with maintainers; [ ma27 ];
-  meta.doc = ./plausible.xml;
+  meta.doc = ./plausible.md;
 }
diff --git a/nixos/modules/services/web-apps/plausible.xml b/nixos/modules/services/web-apps/plausible.xml
deleted file mode 100644
index 92a571b9fbd..00000000000
--- a/nixos/modules/services/web-apps/plausible.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-plausible">
- <title>Plausible</title>
- <para>
-  <link xlink:href="https://plausible.io/">Plausible</link> is a privacy-friendly alternative to
-  Google analytics.
- </para>
- <section xml:id="module-services-plausible-basic-usage">
-  <title>Basic Usage</title>
-  <para>
-   At first, a secret key is needed to be generated. This can be done with e.g.
-   <screen><prompt>$ </prompt>openssl rand -base64 64</screen>
-  </para>
-  <para>
-   After that, <package>plausible</package> can be deployed like this:
-<programlisting>{
-  services.plausible = {
-    <link linkend="opt-services.plausible.enable">enable</link> = true;
-    adminUser = {
-      <link linkend="opt-services.plausible.adminUser.activate">activate</link> = true; <co xml:id='ex-plausible-cfg-activate' />
-      <link linkend="opt-services.plausible.adminUser.email">email</link> = "admin@localhost";
-      <link linkend="opt-services.plausible.adminUser.passwordFile">passwordFile</link> = "/run/secrets/plausible-admin-pwd";
-    };
-    server = {
-      <link linkend="opt-services.plausible.server.baseUrl">baseUrl</link> = "http://analytics.example.org";
-      <link linkend="opt-services.plausible.server.secretKeybaseFile">secretKeybaseFile</link> = "/run/secrets/plausible-secret-key-base"; <co xml:id='ex-plausible-cfg-secretbase' />
-    };
-  };
-}</programlisting>
-   <calloutlist>
-    <callout arearefs='ex-plausible-cfg-activate'>
-     <para>
-      <varname>activate</varname> is used to skip the email verification of the admin-user that's
-      automatically created by <package>plausible</package>. This is only supported if
-      <package>postgresql</package> is configured by the module. This is done by default, but
-      can be turned off with <xref linkend="opt-services.plausible.database.postgres.setup" />.
-     </para>
-    </callout>
-    <callout arearefs='ex-plausible-cfg-secretbase'>
-     <para>
-      <varname>secretKeybaseFile</varname> is a path to the file which contains the secret generated
-      with <package>openssl</package> as described above.
-     </para>
-    </callout>
-   </calloutlist>
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/powerdns-admin.nix b/nixos/modules/services/web-apps/powerdns-admin.nix
index e9f7f41055e..7b6fb06e356 100644
--- a/nixos/modules/services/web-apps/powerdns-admin.nix
+++ b/nixos/modules/services/web-apps/powerdns-admin.nix
@@ -78,7 +78,8 @@ in
       environment.PYTHONPATH = pkgs.powerdns-admin.pythonPath;
       serviceConfig = {
         ExecStart = "${pkgs.powerdns-admin}/bin/powerdns-admin --pid /run/powerdns-admin/pid ${escapeShellArgs cfg.extraArgs}";
-        ExecStartPre = "${pkgs.coreutils}/bin/env FLASK_APP=${pkgs.powerdns-admin}/share/powerdnsadmin/__init__.py ${pkgs.python3Packages.flask}/bin/flask db upgrade -d ${pkgs.powerdns-admin}/share/migrations";
+        # Set environment variables only for starting flask database upgrade
+        ExecStartPre = "${pkgs.coreutils}/bin/env FLASK_APP=${pkgs.powerdns-admin}/share/powerdnsadmin/__init__.py SESSION_TYPE= ${pkgs.python3Packages.flask}/bin/flask db upgrade -d ${pkgs.powerdns-admin}/share/migrations";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         ExecStop = "${pkgs.coreutils}/bin/kill -TERM $MAINPID";
         PIDFile = "/run/powerdns-admin/pid";
diff --git a/nixos/modules/services/web-apps/restya-board.nix b/nixos/modules/services/web-apps/restya-board.nix
index 4b32f06826e..959bcbc5c9f 100644
--- a/nixos/modules/services/web-apps/restya-board.nix
+++ b/nixos/modules/services/web-apps/restya-board.nix
@@ -263,8 +263,8 @@ in
       serviceConfig.RemainAfterExit = true;
 
       wantedBy = [ "multi-user.target" ];
-      requires = if cfg.database.host == null then [] else [ "postgresql.service" ];
-      after = [ "network.target" ] ++ (if cfg.database.host == null then [] else [ "postgresql.service" ]);
+      requires = lib.optional (cfg.database.host != null) "postgresql.service";
+      after = [ "network.target" ] ++ (lib.optional (cfg.database.host != null) "postgresql.service");
 
       script = ''
         rm -rf "${runDir}"
diff --git a/nixos/modules/services/web-apps/sftpgo.nix b/nixos/modules/services/web-apps/sftpgo.nix
new file mode 100644
index 00000000000..846478ecbd6
--- /dev/null
+++ b/nixos/modules/services/web-apps/sftpgo.nix
@@ -0,0 +1,375 @@
+{ options, config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sftpgo;
+  defaultUser = "sftpgo";
+  settingsFormat = pkgs.formats.json {};
+  configFile = settingsFormat.generate "sftpgo.json" cfg.settings;
+  hasPrivilegedPorts = any (port: port > 0 && port < 1024) (
+    catAttrs "port" (cfg.settings.httpd.bindings
+      ++ cfg.settings.ftpd.bindings
+      ++ cfg.settings.sftpd.bindings
+      ++ cfg.settings.webdavd.bindings
+    )
+  );
+in
+{
+  options.services.sftpgo = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = mdDoc "sftpgo";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.sftpgo;
+      defaultText = literalExpression "pkgs.sftpgo";
+      description = mdDoc ''
+        Which SFTPGo package to use.
+      '';
+    };
+
+    extraArgs = mkOption {
+      type = with types; listOf str;
+      default = [];
+      description = mdDoc ''
+        Additional command line arguments to pass to the sftpgo daemon.
+      '';
+      example = [ "--log-level" "info" ];
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/sftpgo";
+      description = mdDoc ''
+        The directory where SFTPGo stores its data files.
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = mdDoc ''
+        User account name under which SFTPGo runs.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = mdDoc ''
+        Group name under which SFTPGo runs.
+      '';
+    };
+
+    loadDataFile = mkOption {
+      default = null;
+      type = with types; nullOr path;
+      description = mdDoc ''
+        Path to a json file containing users and folders to load (or update) on startup.
+        Check the [documentation](https://github.com/drakkan/sftpgo/blob/main/docs/full-configuration.md)
+        for the `--loaddata-from` command line argument for more info.
+      '';
+    };
+
+    settings = mkOption {
+      default = {};
+      description = mdDoc ''
+        The primary sftpgo configuration. See the
+        [configuration reference](https://github.com/drakkan/sftpgo/blob/main/docs/full-configuration.md)
+        for possible values.
+      '';
+      type = with types; submodule {
+        freeformType = settingsFormat.type;
+        options = {
+          httpd.bindings = mkOption {
+            default = [];
+            description = mdDoc ''
+              Configure listen addresses and ports for httpd.
+            '';
+            type = types.listOf (types.submodule {
+              freeformType = settingsFormat.type;
+              options = {
+                address = mkOption {
+                  type = types.str;
+                  default = "127.0.0.1";
+                  description = mdDoc ''
+                    Network listen address. Leave blank to listen on all available network interfaces.
+                    On *NIX you can specify an absolute path to listen on a Unix-domain socket.
+                  '';
+                };
+
+                port = mkOption {
+                  type = types.port;
+                  default = 8080;
+                  description = mdDoc ''
+                    The port for serving HTTP(S) requests.
+
+                    Setting the port to `0` disables listening on this interface binding.
+                  '';
+                };
+
+                enable_web_admin = mkOption {
+                  type = types.bool;
+                  default = true;
+                  description = mdDoc ''
+                    Enable the built-in web admin for this interface binding.
+                  '';
+                };
+
+                enable_web_client = mkOption {
+                  type = types.bool;
+                  default = true;
+                  description = mdDoc ''
+                    Enable the built-in web client for this interface binding.
+                  '';
+                };
+              };
+            });
+          };
+
+          ftpd.bindings = mkOption {
+            default = [];
+            description = mdDoc ''
+              Configure listen addresses and ports for ftpd.
+            '';
+            type = types.listOf (types.submodule {
+              freeformType = settingsFormat.type;
+              options = {
+                address = mkOption {
+                  type = types.str;
+                  default = "127.0.0.1";
+                  description = mdDoc ''
+                    Network listen address. Leave blank to listen on all available network interfaces.
+                    On *NIX you can specify an absolute path to listen on a Unix-domain socket.
+                  '';
+                };
+
+                port = mkOption {
+                  type = types.port;
+                  default = 0;
+                  description = mdDoc ''
+                    The port for serving FTP requests.
+
+                    Setting the port to `0` disables listening on this interface binding.
+                  '';
+                };
+              };
+            });
+          };
+
+          sftpd.bindings = mkOption {
+            default = [];
+            description = mdDoc ''
+              Configure listen addresses and ports for sftpd.
+            '';
+            type = types.listOf (types.submodule {
+              freeformType = settingsFormat.type;
+              options = {
+                address = mkOption {
+                  type = types.str;
+                  default = "127.0.0.1";
+                  description = mdDoc ''
+                    Network listen address. Leave blank to listen on all available network interfaces.
+                    On *NIX you can specify an absolute path to listen on a Unix-domain socket.
+                  '';
+                };
+
+                port = mkOption {
+                  type = types.port;
+                  default = 0;
+                  description = mdDoc ''
+                    The port for serving SFTP requests.
+
+                    Setting the port to `0` disables listening on this interface binding.
+                  '';
+                };
+              };
+            });
+          };
+
+          webdavd.bindings = mkOption {
+            default = [];
+            description = mdDoc ''
+              Configure listen addresses and ports for webdavd.
+            '';
+            type = types.listOf (types.submodule {
+              freeformType = settingsFormat.type;
+              options = {
+                address = mkOption {
+                  type = types.str;
+                  default = "127.0.0.1";
+                  description = mdDoc ''
+                    Network listen address. Leave blank to listen on all available network interfaces.
+                    On *NIX you can specify an absolute path to listen on a Unix-domain socket.
+                  '';
+                };
+
+                port = mkOption {
+                  type = types.port;
+                  default = 0;
+                  description = mdDoc ''
+                    The port for serving WebDAV requests.
+
+                    Setting the port to `0` disables listening on this interface binding.
+                  '';
+                };
+              };
+            });
+          };
+
+          smtp = mkOption {
+            default = {};
+            description = mdDoc ''
+              SMTP configuration section.
+            '';
+            type = types.submodule {
+              freeformType = settingsFormat.type;
+              options = {
+                host = mkOption {
+                  type = types.str;
+                  default = "";
+                  description = mdDoc ''
+                    Location of SMTP email server. Leave empty to disable email sending capabilities.
+                  '';
+                };
+
+                port = mkOption {
+                  type = types.port;
+                  default = 465;
+                  description = mdDoc "Port of the SMTP Server.";
+                };
+
+                encryption = mkOption {
+                  type = types.enum [ 0 1 2 ];
+                  default = 1;
+                  description = mdDoc ''
+                    Encryption scheme:
+                    - `0`: No encryption
+                    - `1`: TLS
+                    - `2`: STARTTLS
+                  '';
+                };
+
+                auth_type = mkOption {
+                  type = types.enum [ 0 1 2 ];
+                  default = 0;
+                  description = mdDoc ''
+                    - `0`: Plain
+                    - `1`: Login
+                    - `2`: CRAM-MD5
+                  '';
+                };
+
+                user = mkOption {
+                  type = types.str;
+                  default = "sftpgo";
+                  description = mdDoc "SMTP username.";
+                };
+
+                from = mkOption {
+                  type = types.str;
+                  default = "SFTPGo <sftpgo@example.com>";
+                  description = mdDoc ''
+                    From address.
+                  '';
+                };
+              };
+            };
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.sftpgo.settings = (mapAttrs (name: mkDefault) {
+      ftpd.bindings = [{ port = 0; }];
+      httpd.bindings = [{ port = 0; }];
+      sftpd.bindings = [{ port = 0; }];
+      webdavd.bindings = [{ port = 0; }];
+      httpd.openapi_path = "${cfg.package}/share/sftpgo/openapi";
+      httpd.templates_path = "${cfg.package}/share/sftpgo/templates";
+      httpd.static_files_path = "${cfg.package}/share/sftpgo/static";
+      smtp.templates_path = "${cfg.package}/share/sftpgo/templates";
+    });
+
+    users = optionalAttrs (cfg.user == defaultUser) {
+      users = {
+        ${defaultUser} = {
+          description = "SFTPGo system user";
+          isSystemUser = true;
+          group = defaultUser;
+          home = cfg.dataDir;
+        };
+      };
+
+      groups = {
+        ${defaultUser} = {
+          members = [ defaultUser ];
+        };
+      };
+    };
+
+    systemd.services.sftpgo = {
+      description = "SFTPGo daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        SFTPGO_CONFIG_FILE = mkDefault configFile;
+        SFTPGO_LOG_FILE_PATH = mkDefault ""; # log to journal
+        SFTPGO_LOADDATA_FROM = mkIf (cfg.loadDataFile != null) cfg.loadDataFile;
+      };
+
+      serviceConfig = mkMerge [
+        ({
+          Type = "simple";
+          User = cfg.user;
+          Group = cfg.group;
+          WorkingDirectory = cfg.dataDir;
+          ReadWritePaths = [ cfg.dataDir ];
+          LimitNOFILE = 8192; # taken from upstream
+          KillMode = "mixed";
+          ExecStart = "${cfg.package}/bin/sftpgo serve ${utils.escapeSystemdExecArgs cfg.extraArgs}";
+          ExecReload = "${pkgs.util-linux}/bin/kill -s HUP $MAINPID";
+
+          # Service hardening
+          CapabilityBoundingSet = [ (optionalString hasPrivilegedPorts "CAP_NET_BIND_SERVICE") ];
+          DevicePolicy = "closed";
+          LockPersonality = true;
+          NoNewPrivileges = true;
+          PrivateDevices = true;
+          PrivateTmp = true;
+          ProcSubset = "pid";
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          ProtectSystem = "strict";
+          RemoveIPC = true;
+          RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [ "@system-service" "~@privileged" ];
+          UMask = "0077";
+        })
+        (mkIf hasPrivilegedPorts {
+          AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        })
+        (mkIf (cfg.dataDir == options.services.sftpgo.dataDir.default) {
+          StateDirectory = baseNameOf cfg.dataDir;
+        })
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/snipe-it.nix b/nixos/modules/services/web-apps/snipe-it.nix
index 93b0aafab64..e861a418519 100644
--- a/nixos/modules/services/web-apps/snipe-it.nix
+++ b/nixos/modules/services/web-apps/snipe-it.nix
@@ -15,6 +15,8 @@ let
 
   tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
 
+  inherit (snipe-it.passthru) phpPackage;
+
   # shell script for local administration
   artisan = pkgs.writeScriptBin "snipe-it" ''
     #! ${pkgs.runtimeShell}
@@ -23,7 +25,7 @@ let
     if [[ "$USER" != ${user} ]]; then
       sudo='exec /run/wrappers/bin/sudo -u ${user}'
     fi
-    $sudo ${pkgs.php}/bin/php artisan $*
+    $sudo ${phpPackage}/bin/php artisan $*
   '';
 in {
   options.services.snipe-it = {
@@ -340,8 +342,7 @@ in {
     };
 
     services.phpfpm.pools.snipe-it = {
-      inherit user group;
-      phpPackage = pkgs.php81;
+      inherit user group phpPackage;
       phpOptions = ''
         post_max_size = ${cfg.maxUploadSize}
         upload_max_filesize = ${cfg.maxUploadSize}
@@ -450,7 +451,7 @@ in {
           rm "${cfg.dataDir}"/bootstrap/cache/*.php || true
 
           # migrate db
-          ${pkgs.php}/bin/php artisan migrate --force
+          ${phpPackage}/bin/php artisan migrate --force
 
           # A placeholder file for invalid barcodes
           invalid_barcode_location="${cfg.dataDir}/public/uploads/barcodes/invalid_barcode.gif"
diff --git a/nixos/modules/services/web-apps/tt-rss.nix b/nixos/modules/services/web-apps/tt-rss.nix
index 6f494fae4cc..3102e6a4695 100644
--- a/nixos/modules/services/web-apps/tt-rss.nix
+++ b/nixos/modules/services/web-apps/tt-rss.nix
@@ -534,7 +534,7 @@ let
     services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
       ${poolName} = {
         inherit (cfg) user;
-        phpPackage = pkgs.php80;
+        phpPackage = pkgs.php81;
         settings = mapAttrs (name: mkDefault) {
           "listen.owner" = "nginx";
           "listen.group" = "nginx";
diff --git a/nixos/modules/services/web-apps/vikunja.nix b/nixos/modules/services/web-apps/vikunja.nix
index c3552200d4e..8bc8e8c2925 100644
--- a/nixos/modules/services/web-apps/vikunja.nix
+++ b/nixos/modules/services/web-apps/vikunja.nix
@@ -56,6 +56,11 @@ in {
       type = types.str;
       description = lib.mdDoc "The Hostname under which the frontend is running.";
     };
+    port = mkOption {
+      type = types.port;
+      default = 3456;
+      description = lib.mdDoc "The TCP port exposed by the API.";
+    };
 
     settings = mkOption {
       type = format.type;
@@ -101,6 +106,7 @@ in {
         inherit (cfg.database) type host user database path;
       };
       service = {
+        interface = ":${toString cfg.port}";
         frontendurl = "${cfg.frontendScheme}://${cfg.frontendHostname}/";
       };
       files = {
@@ -132,7 +138,7 @@ in {
           tryFiles = "try_files $uri $uri/ /";
         };
         "~* ^/(api|dav|\\.well-known)/" = {
-          proxyPass = "http://localhost:3456";
+          proxyPass = "http://localhost:${toString cfg.port}";
           extraConfig = ''
             client_max_body_size 20M;
           '';
diff --git a/nixos/modules/services/web-apps/wiki-js.nix b/nixos/modules/services/web-apps/wiki-js.nix
index b6e5b4594f1..631740f51ce 100644
--- a/nixos/modules/services/web-apps/wiki-js.nix
+++ b/nixos/modules/services/web-apps/wiki-js.nix
@@ -113,7 +113,13 @@ in {
       documentation = [ "https://docs.requarks.io/" ];
       wantedBy = [ "multi-user.target" ];
 
-      path = with pkgs; [ coreutils ];
+      path = with pkgs; [
+        # Needed for git storage.
+        git
+        # Needed for git+ssh storage.
+        openssh
+      ];
+
       preStart = ''
         ln -sf ${configFile} /var/lib/${cfg.stateDirectoryName}/config.yml
         ln -sf ${pkgs.wiki-js}/server /var/lib/${cfg.stateDirectoryName}
@@ -127,7 +133,7 @@ in {
         WorkingDirectory = "/var/lib/${cfg.stateDirectoryName}";
         DynamicUser = true;
         PrivateTmp = true;
-        ExecStart = "${pkgs.nodejs-16_x}/bin/node ${pkgs.wiki-js}/server";
+        ExecStart = "${pkgs.nodejs_18}/bin/node ${pkgs.wiki-js}/server";
       };
     };
   };
diff --git a/nixos/modules/services/web-apps/wordpress.nix b/nixos/modules/services/web-apps/wordpress.nix
index 43a6d7e75dc..d4c987da114 100644
--- a/nixos/modules/services/web-apps/wordpress.nix
+++ b/nixos/modules/services/web-apps/wordpress.nix
@@ -32,35 +32,59 @@ let
       # Since hard linking directories is not allowed, copying is the next best thing.
 
       # copy additional plugin(s), theme(s) and language(s)
-      ${concatMapStringsSep "\n" (theme: "cp -r ${theme} $out/share/wordpress/wp-content/themes/${theme.name}") cfg.themes}
-      ${concatMapStringsSep "\n" (plugin: "cp -r ${plugin} $out/share/wordpress/wp-content/plugins/${plugin.name}") cfg.plugins}
+      ${concatStringsSep "\n" (mapAttrsToList (name: theme: "cp -r ${theme} $out/share/wordpress/wp-content/themes/${name}") cfg.themes)}
+      ${concatStringsSep "\n" (mapAttrsToList (name: plugin: "cp -r ${plugin} $out/share/wordpress/wp-content/plugins/${name}") cfg.plugins)}
       ${concatMapStringsSep "\n" (language: "cp -r ${language} $out/share/wordpress/wp-content/languages/") cfg.languages}
     '';
   };
 
-  wpConfig = hostName: cfg: pkgs.writeText "wp-config-${hostName}.php" ''
-    <?php
-      define('DB_NAME', '${cfg.database.name}');
-      define('DB_HOST', '${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}');
-      define('DB_USER', '${cfg.database.user}');
-      ${optionalString (cfg.database.passwordFile != null) "define('DB_PASSWORD', file_get_contents('${cfg.database.passwordFile}'));"}
-      define('DB_CHARSET', 'utf8');
-      $table_prefix  = '${cfg.database.tablePrefix}';
-
-      require_once('${stateDir hostName}/secret-keys.php');
-
-      # wordpress is installed onto a read-only file system
-      define('DISALLOW_FILE_EDIT', true);
-      define('AUTOMATIC_UPDATER_DISABLED', true);
-
-      ${cfg.extraConfig}
-
-      if ( !defined('ABSPATH') )
-        define('ABSPATH', dirname(__FILE__) . '/');
+  mergeConfig = cfg: {
+    # wordpress is installed onto a read-only file system
+    DISALLOW_FILE_EDIT = true;
+    AUTOMATIC_UPDATER_DISABLED = true;
+    DB_NAME = cfg.database.name;
+    DB_HOST = "${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}";
+    DB_USER = cfg.database.user;
+    DB_CHARSET = "utf8";
+    # Always set DB_PASSWORD even when passwordFile is not set. This is the
+    # default Wordpress behaviour.
+    DB_PASSWORD =  if (cfg.database.passwordFile != null) then { _file = cfg.database.passwordFile; } else "";
+  } // cfg.settings;
+
+  wpConfig = hostName: cfg: let
+    conf_gen = c: mapAttrsToList (k: v: "define('${k}', ${mkPhpValue v});") cfg.mergedConfig;
+  in pkgs.writeTextFile {
+    name = "wp-config-${hostName}.php";
+    text = ''
+      <?php
+        $table_prefix  = '${cfg.database.tablePrefix}';
+
+        require_once('${stateDir hostName}/secret-keys.php');
+
+        ${cfg.extraConfig}
+        ${concatStringsSep "\n" (conf_gen cfg.mergedConfig)}
+
+        if ( !defined('ABSPATH') )
+          define('ABSPATH', dirname(__FILE__) . '/');
+
+        require_once(ABSPATH . 'wp-settings.php');
+      ?>
+    '';
+    checkPhase = "${pkgs.php81}/bin/php --syntax-check $target";
+  };
 
-      require_once(ABSPATH . 'wp-settings.php');
-    ?>
-  '';
+  mkPhpValue = v: let
+    isHasAttr = s: isAttrs v && hasAttr s v;
+  in
+    if isString v then escapeShellArg v
+    # NOTE: If any value contains a , (comma) this will not get escaped
+    else if isList v && any lib.strings.isCoercibleToString v then escapeShellArg (concatMapStringsSep "," toString v)
+    else if isInt v then toString v
+    else if isBool v then boolToString v
+    else if isHasAttr "_file" then "trim(file_get_contents(${lib.escapeShellArg v._file}))"
+    else if isHasAttr "_raw" then v._raw
+    else abort "The Wordpress config value ${lib.generators.toPretty {} v} can not be encoded."
+  ;
 
   secretsVars = [ "AUTH_KEY" "SECURE_AUTH_KEY" "LOGGED_IN_KEY" "NONCE_KEY" "AUTH_SALT" "SECURE_AUTH_SALT" "LOGGED_IN_SALT" "NONCE_SALT" ];
   secretsScript = hostStateDir: ''
@@ -77,7 +101,7 @@ let
     fi
   '';
 
-  siteOpts = { lib, name, ... }:
+  siteOpts = { lib, name, config, ... }:
     {
       options = {
         package = mkOption {
@@ -106,62 +130,45 @@ let
         };
 
         plugins = mkOption {
-          type = types.listOf types.path;
-          default = [];
+          type = with types; coercedTo
+            (listOf path)
+            (l: warn "setting this option with a list is deprecated"
+              listToAttrs (map (p: nameValuePair (p.name or (throw "${p} does not have a name")) p) l))
+            (attrsOf path);
+          default = {};
           description = lib.mdDoc ''
-            List of path(s) to respective plugin(s) which are copied from the 'plugins' directory.
+            Path(s) to respective plugin(s) which are copied from the 'plugins' directory.
 
             ::: {.note}
             These plugins need to be packaged before use, see example.
             :::
           '';
           example = literalExpression ''
-            let
-              # Wordpress plugin 'embed-pdf-viewer' installation example
-              embedPdfViewerPlugin = pkgs.stdenv.mkDerivation {
-                name = "embed-pdf-viewer-plugin";
-                # Download the theme from the wordpress site
-                src = pkgs.fetchurl {
-                  url = "https://downloads.wordpress.org/plugin/embed-pdf-viewer.2.0.3.zip";
-                  sha256 = "1rhba5h5fjlhy8p05zf0p14c9iagfh96y91r36ni0rmk6y891lyd";
-                };
-                # We need unzip to build this package
-                nativeBuildInputs = [ pkgs.unzip ];
-                # Installing simply means copying all files to the output directory
-                installPhase = "mkdir -p $out; cp -R * $out/";
-              };
-            # And then pass this theme to the themes list like this:
-            in [ embedPdfViewerPlugin ]
+            {
+              inherit (pkgs.wordpressPackages.plugins) embed-pdf-viewer-plugin;
+            }
           '';
         };
 
         themes = mkOption {
-          type = types.listOf types.path;
-          default = [];
+          type = with types; coercedTo
+            (listOf path)
+            (l: warn "setting this option with a list is deprecated"
+              listToAttrs (map (p: nameValuePair (p.name or (throw "${p} does not have a name")) p) l))
+            (attrsOf path);
+          default = { inherit (pkgs.wordpressPackages.themes) twentytwentythree; };
+          defaultText = literalExpression "{ inherit (pkgs.wordpressPackages.themes) twentytwentythree; }";
           description = lib.mdDoc ''
-            List of path(s) to respective theme(s) which are copied from the 'theme' directory.
+            Path(s) to respective theme(s) which are copied from the 'theme' directory.
 
             ::: {.note}
             These themes need to be packaged before use, see example.
             :::
           '';
           example = literalExpression ''
-            let
-              # Let's package the responsive theme
-              responsiveTheme = pkgs.stdenv.mkDerivation {
-                name = "responsive-theme";
-                # Download the theme from the wordpress site
-                src = pkgs.fetchurl {
-                  url = "https://downloads.wordpress.org/theme/responsive.3.14.zip";
-                  sha256 = "0rjwm811f4aa4q43r77zxlpklyb85q08f9c8ns2akcarrvj5ydx3";
-                };
-                # We need unzip to build this package
-                nativeBuildInputs = [ pkgs.unzip ];
-                # Installing simply means copying all files to the output directory
-                installPhase = "mkdir -p $out; cp -R * $out/";
-              };
-            # And then pass this theme to the themes list like this:
-            in [ responsiveTheme ]
+            {
+              inherit (pkgs.wordpressPackages.themes) responsive-theme;
+            }
           '';
         };
 
@@ -283,6 +290,42 @@ let
           '';
         };
 
+        settings = mkOption {
+          type = types.attrsOf types.anything;
+          default = {};
+          description = lib.mdDoc ''
+            Structural Wordpress configuration.
+            Refer to <https://developer.wordpress.org/apis/wp-config-php>
+            for details and supported values.
+          '';
+          example = literalExpression ''
+            {
+              WP_DEFAULT_THEME = "twentytwentytwo";
+              WP_SITEURL = "https://example.org";
+              WP_HOME = "https://example.org";
+              WP_DEBUG = true;
+              WP_DEBUG_DISPLAY = true;
+              WPLANG = "de_DE";
+              FORCE_SSL_ADMIN = true;
+              AUTOMATIC_UPDATER_DISABLED = true;
+            }
+          '';
+        };
+
+        mergedConfig = mkOption {
+          readOnly = true;
+          default = mergeConfig config;
+          defaultText = literalExpression ''
+            {
+              DISALLOW_FILE_EDIT = true;
+              AUTOMATIC_UPDATER_DISABLED = true;
+            }
+          '';
+          description = lib.mdDoc ''
+            Read only representation of the final configuration.
+          '';
+        };
+
         extraConfig = mkOption {
           type = types.lines;
           default = "";
@@ -290,11 +333,16 @@ let
             Any additional text to be appended to the wp-config.php
             configuration file. This is a PHP script. For configuration
             settings, see <https://codex.wordpress.org/Editing_wp-config.php>.
+
+            **Note**: Please pass structured settings via
+            `services.wordpress.sites.${name}.settings` instead.
           '';
           example = ''
-            define( 'AUTOSAVE_INTERVAL', 60 ); // Seconds
+            @ini_set( 'log_errors', 'Off' );
+            @ini_set( 'display_errors', 'On' );
           '';
         };
+
       };
 
       config.virtualHost.hostName = mkDefault name;
diff --git a/nixos/modules/services/web-apps/writefreely.nix b/nixos/modules/services/web-apps/writefreely.nix
index dec00b46f33..a7671aa717f 100644
--- a/nixos/modules/services/web-apps/writefreely.nix
+++ b/nixos/modules/services/web-apps/writefreely.nix
@@ -10,12 +10,11 @@ let
   format = pkgs.formats.ini {
     mkKeyValue = key: value:
       let
-        value' = if builtins.isNull value then
-          ""
-        else if builtins.isBool value then
-          if value == true then "true" else "false"
-        else
-          toString value;
+        value' = lib.optionalString (value != null)
+          (if builtins.isBool value then
+            if value == true then "true" else "false"
+          else
+            toString value);
       in "${key} = ${value'}";
   };
 
diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix
index 50213ec252f..5cc9ef6dd6d 100644
--- a/nixos/modules/services/web-servers/caddy/default.nix
+++ b/nixos/modules/services/web-servers/caddy/default.nix
@@ -14,7 +14,7 @@ let
     in
       ''
         ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} {
-          bind ${concatStringsSep " " hostOpts.listenAddresses}
+          ${optionalString (hostOpts.listenAddresses != [ ]) "bind ${concatStringsSep " " hostOpts.listenAddresses}"}
           ${optionalString (hostOpts.useACMEHost != null) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"}
           log {
             ${hostOpts.logFormat}
@@ -35,11 +35,16 @@ let
 
       Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } ''
         mkdir -p $out
-        ${cfg.package}/bin/caddy fmt ${Caddyfile}/Caddyfile > $out/Caddyfile
+        cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile
+        caddy fmt --overwrite $out/Caddyfile
       '';
     in
       "${if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile}/Caddyfile";
 
+  etcConfigFile = "caddy/caddy_config";
+
+  configPath = "/etc/${etcConfigFile}";
+
   acmeHosts = unique (catAttrs "useACMEHost" acmeVHosts);
 
   mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
@@ -154,11 +159,16 @@ in
       description = lib.mdDoc ''
         Override the configuration file used by Caddy. By default,
         NixOS generates one automatically.
+
+        The configuration file is exposed at {file}`${configPath}`.
       '';
     };
 
     adapter = mkOption {
-      default = null;
+      default = if (builtins.baseNameOf cfg.configFile) == "Caddyfile" then "caddyfile" else null;
+      defaultText = literalExpression ''
+        if (builtins.baseNameOf cfg.configFile) == "Caddyfile" then "caddyfile" else null
+      '';
       example = literalExpression "nginx";
       type = with types; nullOr str;
       description = lib.mdDoc ''
@@ -244,15 +254,23 @@ in
     };
 
     acmeCA = mkOption {
-      default = "https://acme-v02.api.letsencrypt.org/directory";
-      example = "https://acme-staging-v02.api.letsencrypt.org/directory";
+      default = null;
+      example = "https://acme-v02.api.letsencrypt.org/directory";
       type = with types; nullOr str;
       description = lib.mdDoc ''
+        ::: {.note}
+        Sets the [`acme_ca` option](https://caddyserver.com/docs/caddyfile/options#acme-ca)
+        in the global options block of the resulting Caddyfile.
+        :::
+
         The URL to the ACME CA's directory. It is strongly recommended to set
-        this to Let's Encrypt's staging endpoint for testing or development.
+        this to `https://acme-staging-v02.api.letsencrypt.org/directory` for
+        Let's Encrypt's [staging endpoint](https://letsencrypt.org/docs/staging-environment/)
+        while testing or in development.
 
-        Set it to `null` if you want to write a more
-        fine-grained configuration manually.
+        Value `null` should be prefered for production setups,
+        as it omits the `acme_ca` option to enable
+        [automatic issuer fallback](https://caddyserver.com/docs/automatic-https#issuer-fallback).
       '';
     };
 
@@ -266,6 +284,21 @@ in
       '';
     };
 
+    enableReload = mkOption {
+      default = true;
+      type = types.bool;
+      description = lib.mdDoc ''
+        Reload Caddy instead of restarting it when configuration file changes.
+
+        Note that enabling this option requires the [admin API](https://caddyserver.com/docs/caddyfile/options#admin)
+        to not be turned off.
+
+        If you enable this option, consider setting [`grace_period`](https://caddyserver.com/docs/caddyfile/options#grace-period)
+        to a non-infinite value in {option}`services.caddy.globalConfig`
+        to prevent Caddy waiting for active connections to finish,
+        which could delay the reload essentially indefinitely.
+      '';
+    };
   };
 
   # implementation
@@ -302,13 +335,16 @@ in
       wantedBy = [ "multi-user.target" ];
       startLimitIntervalSec = 14400;
       startLimitBurst = 10;
+      reloadTriggers = optional cfg.enableReload cfg.configFile;
 
-      serviceConfig = {
+      serviceConfig = let
+        runOptions = ''--config ${configPath} ${optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"}'';
+      in {
         # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart=
         # If the empty string is assigned to this option, the list of commands to start is reset, prior assignments of this option will have no effect.
-        ExecStart = [ "" ''${cfg.package}/bin/caddy run --config ${cfg.configFile} ${optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"} ${optionalString cfg.resume "--resume"}'' ];
-        ExecReload = [ "" ''${cfg.package}/bin/caddy reload --config ${cfg.configFile} ${optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"} --force'' ];
-        ExecStartPre = ''${cfg.package}/bin/caddy validate --config ${cfg.configFile} ${optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"}'';
+        ExecStart = [ "" ''${cfg.package}/bin/caddy run ${runOptions} ${optionalString cfg.resume "--resume"}'' ];
+        # Validating the configuration before applying it ensures we’ll get a proper error that will be reported when switching to the configuration
+        ExecReload = [ "" ''${cfg.package}/bin/caddy reload ${runOptions} --force'' ];
         User = cfg.user;
         Group = cfg.group;
         ReadWriteDirectories = cfg.dataDir;
@@ -344,5 +380,6 @@ in
       in
         listToAttrs certCfg;
 
+    environment.etc.${etcConfigFile}.source = cfg.configFile;
   };
 }
diff --git a/nixos/modules/services/web-servers/fcgiwrap.nix b/nixos/modules/services/web-servers/fcgiwrap.nix
index f9c91fb35db..3a57ef38306 100644
--- a/nixos/modules/services/web-servers/fcgiwrap.nix
+++ b/nixos/modules/services/web-servers/fcgiwrap.nix
@@ -54,7 +54,7 @@ in {
 
       serviceConfig = {
         ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${builtins.toString cfg.preforkProcesses} ${
-          if (cfg.socketType != "unix") then "-s ${cfg.socketType}:${cfg.socketAddress}" else ""
+          optionalString (cfg.socketType != "unix") "-s ${cfg.socketType}:${cfg.socketAddress}"
         }";
       } // (if cfg.user != null && cfg.group != null then {
         User = cfg.user;
diff --git a/nixos/modules/services/web-servers/garage-doc.xml b/nixos/modules/services/web-servers/garage-doc.xml
deleted file mode 100644
index 16f6fde94b5..00000000000
--- a/nixos/modules/services/web-servers/garage-doc.xml
+++ /dev/null
@@ -1,139 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xmlns:xi="http://www.w3.org/2001/XInclude"
-         version="5.0"
-         xml:id="module-services-garage">
- <title>Garage</title>
- <para>
-  <link xlink:href="https://garagehq.deuxfleurs.fr/">Garage</link>
-  is an open-source, self-hostable S3 store, simpler than MinIO, for geodistributed stores.
-  The server setup can be automated using
-  <link linkend="opt-services.garage.enable">services.garage</link>. A
-   client configured to your local Garage instance is available in
-   the global environment as <literal>garage-manage</literal>.
- </para>
- <para>
-  The current default by NixOS is <package>garage_0_8</package> which is also the latest
-  major version available.
- </para>
- <section xml:id="module-services-garage-upgrade-scenarios">
-  <title>General considerations on upgrades</title>
-
-  <para>
-    Garage provides a cookbook documentation on how to upgrade:
-   <link xlink:href="https://garagehq.deuxfleurs.fr/documentation/cookbook/upgrading/">https://garagehq.deuxfleurs.fr/documentation/cookbook/upgrading/</link>
-  </para>
-
- <warning>
-   <para>Garage has two types of upgrades: patch-level upgrades and minor/major version upgrades.</para>
-
-   <para>In all cases, you should read the changelog and ideally test the upgrade on a staging cluster.</para>
-
-   <para>Checking the health of your cluster can be achieved using <literal>garage-manage repair</literal>.</para>
-  </warning>
-
-
- <warning>
-   <para>Until 1.0 is released, patch-level upgrades are considered as minor version upgrades.
-   Minor version upgrades are considered as major version upgrades.
-    i.e. 0.6 to 0.7 is a major version upgrade.</para>
- </warning>
-
- <itemizedlist>
-  <listitem>
-   <formalpara>
-    <title>Straightforward upgrades (patch-level upgrades)</title>
-    <para>
-     Upgrades must be performed one by one, i.e. for each node, stop it, upgrade it : change <link linkend="opt-system.stateVersion">stateVersion</link> or <link linkend="opt-services.garage.package">services.garage.package</link>, restart it if it was not already by switching.
-    </para>
-   </formalpara>
-  </listitem>
-
-  <listitem>
-   <formalpara>
-    <title>Multiple version upgrades</title>
-    <para>
-     Garage do not provide any guarantee on moving more than one major-version forward.
-     E.g., if you're on <literal>0.7</literal>, you cannot upgrade to <literal>0.9</literal>.
-     You need to upgrade to <literal>0.8</literal> first.
-
-     As long as <link linkend="opt-system.stateVersion">stateVersion</link> is declared properly,
-     this is enforced automatically. The module will issue a warning to remind the user to upgrade to latest
-     Garage <emphasis>after</emphasis> that deploy.
-   </para>
-  </formalpara>
- </listitem>
-</itemizedlist>
-</section>
-
-<section xml:id="module-services-garage-advanced-upgrades">
- <title>Advanced upgrades (minor/major version upgrades)</title>
- <para>Here are some baseline instructions to handle advanced upgrades in Garage, when in doubt, please refer to upstream instructions.</para>
-
- <itemizedlist>
-   <listitem><para>Disable API and web access to Garage.</para></listitem>
-   <listitem><para>Perform <literal>garage-manage repair --all-nodes --yes tables</literal> and <literal>garage-manage repair --all-nodes --yes blocks</literal>.</para></listitem>
-   <listitem><para>Verify the resulting logs and check that data is synced properly between all nodes.
-    If you have time, do additional checks (<literal>scrub</literal>, <literal>block_refs</literal>, etc.).</para></listitem>
-   <listitem><para>Check if queues are empty by <literal>garage-manage stats</literal> or through monitoring tools.</para></listitem>
-   <listitem><para>Run <literal>systemctl stop garage</literal> to stop the actual Garage version.</para></listitem>
-   <listitem><para>Backup the metadata folder of ALL your nodes, e.g. for a metadata directory (the default one) in <literal>/var/lib/garage/meta</literal>,
-    you can run <literal>pushd /var/lib/garage; tar -acf meta-v0.7.tar.zst meta/; popd</literal>.</para></listitem>
-   <listitem><para>Run the offline migration: <literal>nix-shell -p garage_0_8 --run "garage offline-repair --yes"</literal>, this can take some time depending on how many objects are stored in your cluster.</para></listitem>
-   <listitem><para>Bump Garage version in your NixOS configuration, either by changing <link linkend="opt-system.stateVersion">stateVersion</link> or bumping <link linkend="opt-services.garage.package">services.garage.package</link>, this should restart Garage automatically.</para></listitem>
-   <listitem><para>Perform <literal>garage-manage repair --all-nodes --yes tables</literal> and <literal>garage-manage repair --all-nodes --yes blocks</literal>.</para></listitem>
-   <listitem><para>Wait for a full table sync to run.</para></listitem>
- </itemizedlist>
-
- <para>
-   Your upgraded cluster should be in a working state, re-enable API and web access.
- </para>
-</section>
-
-<section xml:id="module-services-garage-maintainer-info">
-  <title>Maintainer information</title>
-
-  <para>
-   As stated in the previous paragraph, we must provide a clean upgrade-path for Garage
-   since it cannot move more than one major version forward on a single upgrade. This chapter
-   adds some notes how Garage updates should be rolled out in the future.
-
-   This is inspired from how Nextcloud does it.
-  </para>
-
-  <para>
-   While patch-level updates are no problem and can be done directly in the
-   package-expression (and should be backported to supported stable branches after that),
-   major-releases should be added in a new attribute (e.g. Garage <literal>v0.8.0</literal>
-   should be available in <literal>nixpkgs</literal> as <literal>pkgs.garage_0_8_0</literal>).
-   To provide simple upgrade paths it's generally useful to backport those as well to stable
-   branches. As long as the package-default isn't altered, this won't break existing setups.
-   After that, the versioning-warning in the <literal>garage</literal>-module should be
-   updated to make sure that the
-   <link linkend="opt-services.garage.package">package</link>-option selects the latest version
-   on fresh setups.
-  </para>
-
-  <para>
-   If major-releases will be abandoned by upstream, we should check first if those are needed
-   in NixOS for a safe upgrade-path before removing those. In that case we shold keep those
-   packages, but mark them as insecure in an expression like this (in
-   <literal>&lt;nixpkgs/pkgs/tools/filesystem/garage/default.nix&gt;</literal>):
-<programlisting>/* ... */
-{
-  garage_0_7_3 = generic {
-    version = "0.7.3";
-    sha256 = "0000000000000000000000000000000000000000000000000000";
-    eol = true;
-  };
-}</programlisting>
-  </para>
-
-  <para>
-   Ideally we should make sure that it's possible to jump two NixOS versions forward:
-   i.e. the warnings and the logic in the module should guard a user to upgrade from a
-   Garage on e.g. 22.11 to a Garage on 23.11.
-  </para>
- </section>
-
-</chapter>
diff --git a/nixos/modules/services/web-servers/garage.md b/nixos/modules/services/web-servers/garage.md
new file mode 100644
index 00000000000..3a9b85ce060
--- /dev/null
+++ b/nixos/modules/services/web-servers/garage.md
@@ -0,0 +1,96 @@
+# Garage {#module-services-garage}
+
+[Garage](https://garagehq.deuxfleurs.fr/)
+is an open-source, self-hostable S3 store, simpler than MinIO, for geodistributed stores.
+The server setup can be automated using
+[services.garage](#opt-services.garage.enable). A
+ client configured to your local Garage instance is available in
+ the global environment as `garage-manage`.
+
+The current default by NixOS is `garage_0_8` which is also the latest
+major version available.
+
+## General considerations on upgrades {#module-services-garage-upgrade-scenarios}
+
+Garage provides a cookbook documentation on how to upgrade:
+<https://garagehq.deuxfleurs.fr/documentation/cookbook/upgrading/>
+
+::: {.warning}
+Garage has two types of upgrades: patch-level upgrades and minor/major version upgrades.
+
+In all cases, you should read the changelog and ideally test the upgrade on a staging cluster.
+
+Checking the health of your cluster can be achieved using `garage-manage repair`.
+:::
+
+::: {.warning}
+Until 1.0 is released, patch-level upgrades are considered as minor version upgrades.
+Minor version upgrades are considered as major version upgrades.
+i.e. 0.6 to 0.7 is a major version upgrade.
+:::
+
+  - **Straightforward upgrades (patch-level upgrades).**
+    Upgrades must be performed one by one, i.e. for each node, stop it, upgrade it : change [stateVersion](#opt-system.stateVersion) or [services.garage.package](#opt-services.garage.package), restart it if it was not already by switching.
+  - **Multiple version upgrades.**
+    Garage do not provide any guarantee on moving more than one major-version forward.
+    E.g., if you're on `0.7`, you cannot upgrade to `0.9`.
+    You need to upgrade to `0.8` first.
+    As long as [stateVersion](#opt-system.stateVersion) is declared properly,
+    this is enforced automatically. The module will issue a warning to remind the user to upgrade to latest
+    Garage *after* that deploy.
+
+## Advanced upgrades (minor/major version upgrades) {#module-services-garage-advanced-upgrades}
+
+Here are some baseline instructions to handle advanced upgrades in Garage, when in doubt, please refer to upstream instructions.
+
+  - Disable API and web access to Garage.
+  - Perform `garage-manage repair --all-nodes --yes tables` and `garage-manage repair --all-nodes --yes blocks`.
+  - Verify the resulting logs and check that data is synced properly between all nodes.
+    If you have time, do additional checks (`scrub`, `block_refs`, etc.).
+  - Check if queues are empty by `garage-manage stats` or through monitoring tools.
+  - Run `systemctl stop garage` to stop the actual Garage version.
+  - Backup the metadata folder of ALL your nodes, e.g. for a metadata directory (the default one) in `/var/lib/garage/meta`,
+    you can run `pushd /var/lib/garage; tar -acf meta-v0.7.tar.zst meta/; popd`.
+  - Run the offline migration: `nix-shell -p garage_0_8 --run "garage offline-repair --yes"`, this can take some time depending on how many objects are stored in your cluster.
+  - Bump Garage version in your NixOS configuration, either by changing [stateVersion](#opt-system.stateVersion) or bumping [services.garage.package](#opt-services.garage.package), this should restart Garage automatically.
+  - Perform `garage-manage repair --all-nodes --yes tables` and `garage-manage repair --all-nodes --yes blocks`.
+  - Wait for a full table sync to run.
+
+Your upgraded cluster should be in a working state, re-enable API and web access.
+
+## Maintainer information {#module-services-garage-maintainer-info}
+
+As stated in the previous paragraph, we must provide a clean upgrade-path for Garage
+since it cannot move more than one major version forward on a single upgrade. This chapter
+adds some notes how Garage updates should be rolled out in the future.
+This is inspired from how Nextcloud does it.
+
+While patch-level updates are no problem and can be done directly in the
+package-expression (and should be backported to supported stable branches after that),
+major-releases should be added in a new attribute (e.g. Garage `v0.8.0`
+should be available in `nixpkgs` as `pkgs.garage_0_8_0`).
+To provide simple upgrade paths it's generally useful to backport those as well to stable
+branches. As long as the package-default isn't altered, this won't break existing setups.
+After that, the versioning-warning in the `garage`-module should be
+updated to make sure that the
+[package](#opt-services.garage.package)-option selects the latest version
+on fresh setups.
+
+If major-releases will be abandoned by upstream, we should check first if those are needed
+in NixOS for a safe upgrade-path before removing those. In that case we should keep those
+packages, but mark them as insecure in an expression like this (in
+`<nixpkgs/pkgs/tools/filesystem/garage/default.nix>`):
+```
+/* ... */
+{
+  garage_0_7_3 = generic {
+    version = "0.7.3";
+    sha256 = "0000000000000000000000000000000000000000000000000000";
+    eol = true;
+  };
+}
+```
+
+Ideally we should make sure that it's possible to jump two NixOS versions forward:
+i.e. the warnings and the logic in the module should guard a user to upgrade from a
+Garage on e.g. 22.11 to a Garage on 23.11.
diff --git a/nixos/modules/services/web-servers/garage.nix b/nixos/modules/services/web-servers/garage.nix
index d66bcd73150..8b5734b5a2c 100644
--- a/nixos/modules/services/web-servers/garage.nix
+++ b/nixos/modules/services/web-servers/garage.nix
@@ -9,7 +9,7 @@ let
 in
 {
   meta = {
-    doc = ./garage-doc.xml;
+    doc = ./garage.md;
     maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
   };
 
@@ -49,18 +49,18 @@ in
 
           replication_mode = mkOption {
             default = "none";
-            type = types.enum ([ "none" "1" "2" "3" 1 2 3 ]);
+            type = types.enum ([ "none" "1" "2" "3" "2-dangerous" "3-dangerous" "3-degraded" 1 2 3 ]);
             apply = v: toString v;
-            description = lib.mdDoc "Garage replication mode, defaults to none, see: <https://garagehq.deuxfleurs.fr/reference_manual/configuration.html#replication_mode> for reference.";
+            description = lib.mdDoc "Garage replication mode, defaults to none, see: <https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication-mode> for reference.";
           };
         };
       };
-      description = lib.mdDoc "Garage configuration, see <https://garagehq.deuxfleurs.fr/reference_manual/configuration.html> for reference.";
+      description = lib.mdDoc "Garage configuration, see <https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/> for reference.";
     };
 
     package = mkOption {
       # TODO: when 23.05 is released and if Garage 0.9 is the default, put a stateVersion check.
-      default = if versionAtLeast stateVersion "23.05" then pkgs.garage_0_8_0
+      default = if versionAtLeast config.system.stateVersion "23.05" then pkgs.garage_0_8
                 else pkgs.garage_0_7;
       defaultText = literalExpression "pkgs.garage_0_7";
       type = types.package;
@@ -80,6 +80,7 @@ in
       after = [ "network.target" "network-online.target" ];
       wants = [ "network.target" "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ configFile ];
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/garage server";
 
diff --git a/nixos/modules/services/web-servers/jboss/builder.sh b/nixos/modules/services/web-servers/jboss/builder.sh
index 0e5af324c13..ac573089cd5 100644
--- a/nixos/modules/services/web-servers/jboss/builder.sh
+++ b/nixos/modules/services/web-servers/jboss/builder.sh
@@ -1,5 +1,6 @@
 set -e
 
+if [ -e .attrs.sh ]; then source .attrs.sh; fi
 source $stdenv/setup
 
 mkdir -p $out/bin
diff --git a/nixos/modules/services/web-servers/keter/default.nix b/nixos/modules/services/web-servers/keter/default.nix
index 9adbe65de69..3916c486475 100644
--- a/nixos/modules/services/web-servers/keter/default.nix
+++ b/nixos/modules/services/web-servers/keter/default.nix
@@ -1,53 +1,82 @@
 { config, pkgs, lib, ... }:
 let
   cfg = config.services.keter;
+  yaml = pkgs.formats.yaml { };
 in
 {
   meta = {
     maintainers = with lib.maintainers; [ jappie ];
   };
 
+  imports = [
+    (lib.mkRenamedOptionModule [ "services" "keter" "keterRoot" ] [ "services" "keter" "root" ])
+    (lib.mkRenamedOptionModule [ "services" "keter" "keterPackage" ] [ "services" "keter" "package" ])
+  ];
+
   options.services.keter = {
     enable = lib.mkEnableOption (lib.mdDoc ''keter, a web app deployment manager.
 Note that this module only support loading of webapps:
 Keep an old app running and swap the ports when the new one is booted.
 '');
 
-    keterRoot = lib.mkOption {
+    root = lib.mkOption {
       type = lib.types.str;
       default = "/var/lib/keter";
       description = lib.mdDoc "Mutable state folder for keter";
     };
 
-    keterPackage = lib.mkOption {
+    package = lib.mkOption {
       type = lib.types.package;
       default = pkgs.haskellPackages.keter;
       defaultText = lib.literalExpression "pkgs.haskellPackages.keter";
       description = lib.mdDoc "The keter package to be used";
     };
 
+
     globalKeterConfig = lib.mkOption {
-      type = lib.types.attrs;
-      default = {
-        ip-from-header = true;
-        listeners = [{
-          host = "*4";
-          port = 6981;
-        }];
+      type = lib.types.submodule {
+        freeformType = yaml.type;
+        options = {
+          ip-from-header = lib.mkOption {
+            default = true;
+            type = lib.types.bool;
+            description = lib.mdDoc "You want that ip-from-header in the nginx setup case. It allows nginx setting the original ip address rather then it being localhost (due to reverse proxying)";
+          };
+          listeners = lib.mkOption {
+            default = [{ host = "*"; port = 6981; }];
+            type = lib.types.listOf (lib.types.submodule {
+              options = {
+                host = lib.mkOption {
+                  type = lib.types.str;
+                  description = lib.mdDoc "host";
+                };
+                port = lib.mkOption {
+                  type = lib.types.port;
+                  description = lib.mdDoc "port";
+                };
+              };
+            });
+            description = lib.mdDoc ''
+              You want that ip-from-header in
+              the nginx setup case.
+              It allows nginx setting the original ip address rather
+              then it being localhost (due to reverse proxying).
+              However if you configure keter to accept connections
+              directly you may want to set this to false.'';
+          };
+          rotate-logs = lib.mkOption {
+            default = false;
+            type = lib.types.bool;
+            description = lib.mdDoc ''
+              emits keter logs and it's applications to stderr.
+              which allows journald to capture them.
+              Set to true to let keter put the logs in files
+              (useful on non systemd systems, this is the old approach
+              where keter handled log management)'';
+          };
+        };
       };
-      # You want that ip-from-header in the nginx setup case
-      # so it's not set to 127.0.0.1.
-      # using a port above 1024 allows you to avoid needing CAP_NET_BIND_SERVICE
-      defaultText = lib.literalExpression ''
-        {
-          ip-from-header = true;
-          listeners = [{
-            host = "*4";
-            port = 6981;
-          }];
-        }
-      '';
-      description = lib.mdDoc "Global config for keter";
+      description = lib.mdDoc "Global config for keter, see <https://github.com/snoyberg/keter/blob/master/etc/keter-config.yaml> for reference";
     };
 
     bundle = {
@@ -90,12 +119,12 @@ Keep an old app running and swap the ports when the new one is booted.
 
   config = lib.mkIf cfg.enable (
     let
-      incoming = "${cfg.keterRoot}/incoming";
+      incoming = "${cfg.root}/incoming";
 
 
       globalKeterConfigFile = pkgs.writeTextFile {
         name = "keter-config.yml";
-        text = (lib.generators.toYAML { } (cfg.globalKeterConfig // { root = cfg.keterRoot; }));
+        text = (lib.generators.toYAML { } (cfg.globalKeterConfig // { root = cfg.root; }));
       };
 
       # If things are expected to change often, put it in the bundle!
@@ -122,7 +151,7 @@ Keep an old app running and swap the ports when the new one is booted.
         script = ''
           set -xe
           mkdir -p ${incoming}
-          { tail -F ${cfg.keterRoot}/log/keter/current.log -n 0 & ${cfg.keterPackage}/bin/keter ${globalKeterConfigFile}; }
+          ${lib.getExe cfg.package} ${globalKeterConfigFile};
         '';
         wantedBy = [ "multi-user.target" "nginx.service" ];
 
diff --git a/nixos/modules/services/web-servers/lighttpd/default.nix b/nixos/modules/services/web-servers/lighttpd/default.nix
index 811afe8e0af..0438e12e7da 100644
--- a/nixos/modules/services/web-servers/lighttpd/default.nix
+++ b/nixos/modules/services/web-servers/lighttpd/default.nix
@@ -64,7 +64,7 @@ let
   ];
 
   maybeModuleString = moduleName:
-    if elem moduleName cfg.enableModules then ''"${moduleName}"'' else "";
+    optionalString (elem moduleName cfg.enableModules) ''"${moduleName}"'';
 
   modulesIncludeString = concatStringsSep ",\n"
     (filter (x: x != "") (map maybeModuleString allKnownModules));
@@ -106,15 +106,15 @@ let
       static-file.exclude-extensions = ( ".fcgi", ".php", ".rb", "~", ".inc" )
       index-file.names = ( "index.html" )
 
-      ${if cfg.mod_userdir then ''
+      ${optionalString cfg.mod_userdir ''
         userdir.path = "public_html"
-      '' else ""}
+      ''}
 
-      ${if cfg.mod_status then ''
+      ${optionalString cfg.mod_status ''
         status.status-url = "/server-status"
         status.statistics-url = "/server-statistics"
         status.config-url = "/server-config"
-      '' else ""}
+      ''}
 
       ${cfg.extraConfig}
     '';
diff --git a/nixos/modules/services/web-servers/minio.nix b/nixos/modules/services/web-servers/minio.nix
index 1a9eacb431b..21bec4f63a8 100644
--- a/nixos/modules/services/web-servers/minio.nix
+++ b/nixos/modules/services/web-servers/minio.nix
@@ -60,7 +60,7 @@ in
       '';
     };
 
-    rootCredentialsFile = mkOption  {
+    rootCredentialsFile = mkOption {
       type = types.nullOr types.path;
       default = null;
       description = lib.mdDoc ''
@@ -96,29 +96,62 @@ in
   config = mkIf cfg.enable {
     warnings = optional ((cfg.accessKey != "") || (cfg.secretKey != "")) "services.minio.`accessKey` and services.minio.`secretKey` are deprecated, please use services.minio.`rootCredentialsFile` instead.";
 
-    systemd.tmpfiles.rules = [
-      "d '${cfg.configDir}' - minio minio - -"
-    ] ++ (map (x:  "d '" + x + "' - minio minio - - ") cfg.dataDir);
-
-    systemd.services.minio = {
-      description = "Minio Object Storage";
-      after = [ "network-online.target" ];
-      wantedBy = [ "multi-user.target" ];
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/minio server --json --address ${cfg.listenAddress} --console-address ${cfg.consoleAddress} --config-dir=${cfg.configDir} ${toString cfg.dataDir}";
-        Type = "simple";
-        User = "minio";
-        Group = "minio";
-        LimitNOFILE = 65536;
-        EnvironmentFile = if (cfg.rootCredentialsFile != null) then cfg.rootCredentialsFile
-                          else if ((cfg.accessKey != "") || (cfg.secretKey != "")) then (legacyCredentials cfg)
-                          else null;
+    systemd = lib.mkMerge [{
+      tmpfiles.rules = [
+        "d '${cfg.configDir}' - minio minio - -"
+      ] ++ (map (x: "d '" + x + "' - minio minio - - ") cfg.dataDir);
+
+      services.minio = {
+        description = "Minio Object Storage";
+        after = [ "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          ExecStart = "${cfg.package}/bin/minio server --json --address ${cfg.listenAddress} --console-address ${cfg.consoleAddress} --config-dir=${cfg.configDir} ${toString cfg.dataDir}";
+          Type = "simple";
+          User = "minio";
+          Group = "minio";
+          LimitNOFILE = 65536;
+          EnvironmentFile =
+            if (cfg.rootCredentialsFile != null) then cfg.rootCredentialsFile
+            else if ((cfg.accessKey != "") || (cfg.secretKey != "")) then (legacyCredentials cfg)
+            else null;
+        };
+        environment = {
+          MINIO_REGION = "${cfg.region}";
+          MINIO_BROWSER = "${if cfg.browser then "on" else "off"}";
+        };
       };
-      environment = {
-        MINIO_REGION = "${cfg.region}";
-        MINIO_BROWSER = "${if cfg.browser then "on" else "off"}";
-      };
-    };
+    }
+
+      (lib.mkIf (cfg.rootCredentialsFile != null) {
+        # The service will fail if the credentials file is missing
+        services.minio.unitConfig.ConditionPathExists = cfg.rootCredentialsFile;
+
+        # The service will not restart if the credentials file has
+        # been changed. This can cause stale root credentials.
+        paths.minio-root-credentials = {
+          wantedBy = [ "multi-user.target" ];
+
+          pathConfig = {
+            PathChanged = [ cfg.rootCredentialsFile ];
+            Unit = "minio-restart.service";
+          };
+        };
+
+        services.minio-restart = {
+          description = "Restart MinIO";
+
+          script = ''
+            systemctl restart minio.service
+          '';
+
+          serviceConfig = {
+            Type = "oneshot";
+            Restart = "on-failure";
+            RestartSec = 5;
+          };
+        };
+      })];
 
     users.users.minio = {
       group = "minio";
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index d31f3d5d465..e87159ba99c 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg = config.services.nginx;
-  certs = config.security.acme.certs;
+  inherit (config.security.acme) certs;
   vhostsConfigs = mapAttrsToList (vhostName: vhostConfig: vhostConfig) virtualHosts;
   acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME || vhostConfig.useACMEHost != null) vhostsConfigs;
   dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
@@ -27,10 +27,11 @@ let
                               else "${certs.${certName}.directory}/chain.pem";
     })
   ) cfg.virtualHosts;
-  enableIPv6 = config.networking.enableIPv6;
+  inherit (config.networking) enableIPv6;
 
   # Mime.types values are taken from brotli sample configuration - https://github.com/google/ngx_brotli
   # and Nginx Server Configs - https://github.com/h5bp/server-configs-nginx
+  # "text/html" is implicitly included in {brotli,gzip,zstd}_types
   compressMimeTypes = [
     "application/atom+xml"
     "application/geo+json"
@@ -55,7 +56,6 @@ let
     "text/calendar"
     "text/css"
     "text/csv"
-    "text/html"
     "text/javascript"
     "text/markdown"
     "text/plain"
@@ -102,22 +102,36 @@ let
     proxy_set_header        X-Forwarded-Server $host;
   '';
 
+  proxyCachePathConfig = concatStringsSep "\n" (mapAttrsToList (name: proxyCachePath: ''
+    proxy_cache_path ${concatStringsSep " " [
+      "/var/cache/nginx/${name}"
+      "keys_zone=${proxyCachePath.keysZoneName}:${proxyCachePath.keysZoneSize}"
+      "levels=${proxyCachePath.levels}"
+      "use_temp_path=${if proxyCachePath.useTempPath then "on" else "off"}"
+      "inactive=${proxyCachePath.inactive}"
+      "max_size=${proxyCachePath.maxSize}"
+    ]};
+  '') (filterAttrs (name: conf: conf.enable) cfg.proxyCachePath));
+
+  toUpstreamParameter = key: value:
+    if builtins.isBool value
+    then lib.optionalString value key
+    else "${key}=${toString value}";
+
   upstreamConfig = toString (flip mapAttrsToList cfg.upstreams (name: upstream: ''
     upstream ${name} {
       ${toString (flip mapAttrsToList upstream.servers (name: server: ''
-        server ${name} ${optionalString server.backup "backup"};
+        server ${name} ${concatStringsSep " " (mapAttrsToList toUpstreamParameter server)};
       ''))}
       ${upstream.extraConfig}
     }
   ''));
 
   commonHttpConfig = ''
-      # The mime type definitions included with nginx are very incomplete, so
-      # we use a list of mime types from the mailcap package, which is also
-      # used by most other Linux distributions by default.
-      include ${pkgs.mailcap}/etc/nginx/mime.types;
+      # Load mime types.
+      include ${cfg.defaultMimeTypes};
       # When recommendedOptimisation is disabled nginx fails to start because the mailmap mime.types database
-      # contains 1026 enries and the default is only 1024. Setting to a higher number to remove the need to
+      # contains 1026 entries and the default is only 1024. Setting to a higher number to remove the need to
       # overwrite it because nginx does not allow duplicated settings.
       types_hash_max_size 4096;
 
@@ -149,7 +163,7 @@ let
       ''}
       ${upstreamConfig}
 
-      ${optionalString (cfg.recommendedOptimisation) ''
+      ${optionalString cfg.recommendedOptimisation ''
         # optimisation
         sendfile on;
         tcp_nopush on;
@@ -161,7 +175,7 @@ let
       ${optionalString (cfg.sslCiphers != null) "ssl_ciphers ${cfg.sslCiphers};"}
       ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"}
 
-      ${optionalString (cfg.recommendedTlsSettings) ''
+      ${optionalString cfg.recommendedTlsSettings ''
         # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate
 
         ssl_session_timeout 1d;
@@ -177,40 +191,44 @@ let
         ssl_stapling_verify on;
       ''}
 
-      ${optionalString (cfg.recommendedBrotliSettings) ''
+      ${optionalString cfg.recommendedBrotliSettings ''
         brotli on;
         brotli_static on;
         brotli_comp_level 5;
         brotli_window 512k;
         brotli_min_length 256;
         brotli_types ${lib.concatStringsSep " " compressMimeTypes};
-        brotli_buffers 32 8k;
       ''}
 
-      ${optionalString (cfg.recommendedGzipSettings) ''
+      ${optionalString cfg.recommendedGzipSettings
+        # https://docs.nginx.com/nginx/admin-guide/web-server/compression/
+      ''
         gzip on;
-        gzip_proxied any;
-        gzip_comp_level 5;
-        gzip_types
-          application/atom+xml
-          application/javascript
-          application/json
-          application/xml
-          application/xml+rss
-          image/svg+xml
-          text/css
-          text/javascript
-          text/plain
-          text/xml;
+        gzip_static on;
         gzip_vary on;
+        gzip_comp_level 5;
+        gzip_min_length 256;
+        gzip_proxied expired no-cache no-store private auth;
+        gzip_types ${lib.concatStringsSep " " compressMimeTypes};
+      ''}
+
+      ${optionalString cfg.recommendedZstdSettings ''
+        zstd on;
+        zstd_comp_level 9;
+        zstd_min_length 256;
+        zstd_static on;
+        zstd_types ${lib.concatStringsSep " " compressMimeTypes};
       ''}
 
-      ${optionalString (cfg.recommendedProxySettings) ''
+      ${optionalString cfg.recommendedProxySettings ''
         proxy_redirect          off;
         proxy_connect_timeout   ${cfg.proxyTimeout};
         proxy_send_timeout      ${cfg.proxyTimeout};
         proxy_read_timeout      ${cfg.proxyTimeout};
         proxy_http_version      1.1;
+        # don't let clients close the keep-alive connection to upstream. See the nginx blog for details:
+        # https://www.nginx.com/blog/avoiding-top-10-nginx-configuration-mistakes/#no-keepalives
+        proxy_set_header        "Connection" "";
         include ${recommendedProxyConfig};
       ''}
 
@@ -239,17 +257,9 @@ let
 
       server_tokens ${if cfg.serverTokens then "on" else "off"};
 
-      ${optionalString (cfg.proxyCache.enable) ''
-        proxy_cache_path /var/cache/nginx keys_zone=${cfg.proxyCache.keysZoneName}:${cfg.proxyCache.keysZoneSize}
-                                          levels=${cfg.proxyCache.levels}
-                                          use_temp_path=${if cfg.proxyCache.useTempPath then "on" else "off"}
-                                          inactive=${cfg.proxyCache.inactive}
-                                          max_size=${cfg.proxyCache.maxSize};
-      ''}
-
       ${cfg.commonHttpConfig}
 
-      ${vhosts}
+      ${proxyCachePathConfig}
 
       ${optionalString cfg.statusPage ''
         server {
@@ -268,6 +278,8 @@ let
         }
       ''}
 
+      ${vhosts}
+
       ${cfg.appendHttpConfig}
     }''}
 
@@ -288,7 +300,7 @@ let
 
   configPath = if cfg.enableReload
     then "/etc/nginx/nginx.conf"
-    else finalConfigFile;
+    else configFile;
 
   execCommand = "${cfg.package}/bin/nginx -c '${configPath}'";
 
@@ -297,33 +309,54 @@ let
         onlySSL = vhost.onlySSL || vhost.enableSSL;
         hasSSL = onlySSL || vhost.addSSL || vhost.forceSSL;
 
+        # First evaluation of defaultListen based on a set of listen lines.
+        mkDefaultListenVhost = listenLines:
+          # If this vhost has SSL or is a SSL rejection host.
+          # We enable a TLS variant for lines without explicit ssl or ssl = true.
+          optionals (hasSSL || vhost.rejectSSL)
+            (map (listen: { port = cfg.defaultSSLListenPort; ssl = true; } // listen)
+            (filter (listen: !(listen ? ssl) || listen.ssl) listenLines))
+          # If this vhost is supposed to serve HTTP
+          # We provide listen lines for those without explicit ssl or ssl = false.
+          ++ optionals (!onlySSL)
+            (map (listen: { port = cfg.defaultHTTPListenPort; ssl = false; } // listen)
+            (filter (listen: !(listen ? ssl) || !listen.ssl) listenLines));
+
         defaultListen =
           if vhost.listen != [] then vhost.listen
           else
+          if cfg.defaultListen != [] then mkDefaultListenVhost
+            # Cleanup nulls which will mess up with //.
+            # TODO: is there a better way to achieve this? i.e. mergeButIgnoreNullPlease?
+            (map (listenLine: filterAttrs (_: v: (v != null)) listenLine) cfg.defaultListen)
+          else
             let addrs = if vhost.listenAddresses != [] then vhost.listenAddresses else cfg.defaultListenAddresses;
-            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);
+            in mkDefaultListenVhost (map (addr: { inherit addr; }) addrs);
+
 
         hostListen =
           if vhost.forceSSL
             then filter (x: x.ssl) defaultListen
             else defaultListen;
 
-        listenString = { addr, port, ssl, extraParameters ? [], ... }:
-          (if ssl && vhost.http3 then "
-          # UDP listener for **QUIC+HTTP/3
-          listen ${addr}:${toString port} http3 "
+        listenString = { addr, port, ssl, proxyProtocol ? false, extraParameters ? [], ... }:
+          # UDP listener for QUIC transport protocol.
+          (optionalString (ssl && vhost.quic) ("
+            listen ${addr}:${toString port} quic "
           + optionalString vhost.default "default_server "
           + optionalString vhost.reuseport "reuseport "
-          + optionalString (extraParameters != []) (concatStringsSep " " extraParameters)
-          + ";" else "")
+          + optionalString (extraParameters != []) (concatStringsSep " "
+            (let inCompatibleParameters = [ "ssl" "proxy_protocol" "http2" ];
+                isCompatibleParameter = param: !(any (p: p == param) inCompatibleParameters);
+            in filter isCompatibleParameter extraParameters))
+          + ";"))
           + "
-
             listen ${addr}:${toString port} "
           + optionalString (ssl && vhost.http2) "http2 "
           + optionalString ssl "ssl "
           + optionalString vhost.default "default_server "
           + optionalString vhost.reuseport "reuseport "
+          + optionalString proxyProtocol "proxy_protocol "
           + optionalString (extraParameters != []) (concatStringsSep " " extraParameters)
           + ";";
 
@@ -362,6 +395,10 @@ let
         server {
           ${concatMapStringsSep "\n" listenString hostListen}
           server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases};
+          ${optionalString (hasSSL && vhost.quic) ''
+            http3 ${if vhost.http3 then "on" else "off"};
+            http3_hq ${if vhost.http3_hq then "on" else "off"};
+          ''}
           ${acmeLocation}
           ${optionalString (vhost.root != null) "root ${vhost.root};"}
           ${optionalString (vhost.globalRedirect != null) ''
@@ -383,9 +420,10 @@ let
             ssl_conf_command Options KTLS;
           ''}
 
-          ${optionalString (hasSSL && vhost.http3) ''
+          ${optionalString (hasSSL && vhost.quic && vhost.http3)
             # Advertise that HTTP/3 is available
-            add_header Alt-Svc 'h3=":443"; ma=86400' always;
+          ''
+            add_header Alt-Svc 'h3=":$server_port"; ma=86400';
           ''}
 
           ${mkBasicAuth vhostName vhost}
@@ -440,38 +478,6 @@ let
   );
 
   mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
-
-  snakeOilCert = pkgs.runCommand "nginx-config-validate-cert" { nativeBuildInputs = [ pkgs.openssl.bin ]; } ''
-    mkdir $out
-    openssl genrsa -des3 -passout pass:xxxxx -out server.pass.key 2048
-    openssl rsa -passin pass:xxxxx -in server.pass.key -out $out/server.key
-    openssl req -new -key $out/server.key -out server.csr \
-    -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com"
-    openssl x509 -req -days 1 -in server.csr -signkey $out/server.key -out $out/server.crt
-  '';
-  validatedConfigFile = pkgs.runCommand "validated-nginx.conf" { nativeBuildInputs = [ cfg.package ]; } ''
-    # nginx absolutely wants to read the certificates even when told to only validate config, so let's provide fake certs
-    sed ${configFile} \
-    -e "s|ssl_certificate .*;|ssl_certificate ${snakeOilCert}/server.crt;|g" \
-    -e "s|ssl_trusted_certificate .*;|ssl_trusted_certificate ${snakeOilCert}/server.crt;|g" \
-    -e "s|ssl_certificate_key .*;|ssl_certificate_key ${snakeOilCert}/server.key;|g" \
-    > conf
-
-    LD_PRELOAD=${pkgs.libredirect}/lib/libredirect.so \
-      NIX_REDIRECTS="/etc/resolv.conf=/dev/null" \
-      nginx -t -c $(readlink -f ./conf) > out 2>&1 || true
-    if ! grep -q "syntax is ok" out; then
-      echo nginx config validation failed.
-      echo config was ${configFile}.
-      echo 'in case of false positive, set `services.nginx.validateConfig` to false.'
-      echo nginx output:
-      cat out
-      exit 1
-    fi
-    cp ${configFile} $out
-  '';
-
-  finalConfigFile = if cfg.validateConfig then validatedConfigFile else configFile;
 in
 
 {
@@ -507,7 +513,8 @@ in
         default = false;
         type = types.bool;
         description = lib.mdDoc ''
-          Enable recommended brotli settings. Learn more about compression in Brotli format [here](https://github.com/google/ngx_brotli/blob/master/README.md).
+          Enable recommended brotli settings.
+          Learn more about compression in Brotli format [here](https://github.com/google/ngx_brotli/).
 
           This adds `pkgs.nginxModules.brotli` to `services.nginx.additionalModules`.
         '';
@@ -518,6 +525,18 @@ in
         type = types.bool;
         description = lib.mdDoc ''
           Enable recommended gzip settings.
+          Learn more about compression in Gzip format [here](https://docs.nginx.com/nginx/admin-guide/web-server/compression/).
+        '';
+      };
+
+      recommendedZstdSettings = mkOption {
+        default = false;
+        type = types.bool;
+        description = lib.mdDoc ''
+          Enable recommended zstd settings.
+          Learn more about compression in Zstd format [here](https://github.com/tokers/zstd-nginx-module).
+
+          This adds `pkgs.nginxModules.zstd` to `services.nginx.additionalModules`.
         '';
       };
 
@@ -538,6 +557,49 @@ in
         '';
       };
 
+      defaultListen = mkOption {
+        type = with types; listOf (submodule {
+          options = {
+            addr = mkOption {
+              type = str;
+              description = lib.mdDoc "IP address.";
+            };
+            port = mkOption {
+              type = nullOr port;
+              description = lib.mdDoc "Port number.";
+              default = null;
+            };
+            ssl  = mkOption {
+              type = nullOr bool;
+              default = null;
+              description = lib.mdDoc "Enable SSL.";
+            };
+            proxyProtocol = mkOption {
+              type = bool;
+              description = lib.mdDoc "Enable PROXY protocol.";
+              default = false;
+            };
+            extraParameters = mkOption {
+              type = listOf str;
+              description = lib.mdDoc "Extra parameters of this listen directive.";
+              default = [ ];
+              example = [ "backlog=1024" "deferred" ];
+            };
+          };
+        });
+        default = [];
+        example = literalExpression ''[
+          { addr = "10.0.0.12"; proxyProtocol = true; ssl = true; }
+          { addr = "0.0.0.0"; }
+          { addr = "[::0]"; }
+        ]'';
+        description = lib.mdDoc ''
+          If vhosts do not specify listen, use these addresses by default.
+          This option takes precedence over {option}`defaultListenAddresses` and
+          other listen-related defaults options.
+        '';
+      };
+
       defaultListenAddresses = mkOption {
         type = types.listOf types.str;
         default = [ "0.0.0.0" ] ++ optional enableIPv6 "[::0]";
@@ -545,6 +607,7 @@ in
         example = literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]'';
         description = lib.mdDoc ''
           If vhosts do not specify listenAddresses, use these addresses by default.
+          This is akin to writing `defaultListen = [ { addr = "0.0.0.0" } ]`.
         '';
       };
 
@@ -566,6 +629,18 @@ in
         '';
       };
 
+      defaultMimeTypes = mkOption {
+        type = types.path;
+        default = "${pkgs.mailcap}/etc/nginx/mime.types";
+        defaultText = literalExpression "$''{pkgs.mailcap}/etc/nginx/mime.types";
+        example = literalExpression "$''{pkgs.nginx}/conf/mime.types";
+        description = lib.mdDoc ''
+          Default MIME types for NGINX, as MIME types definitions from NGINX are very incomplete,
+          we use by default the ones bundled in the mailcap package, used by most of the other
+          Linux distributions.
+        '';
+      };
+
       package = mkOption {
         default = pkgs.nginxStable;
         defaultText = literalExpression "pkgs.nginxStable";
@@ -580,17 +655,6 @@ in
         '';
       };
 
-      validateConfig = mkOption {
-        # FIXME: re-enable if we can make of the configurations work.
-        #default = pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform;
-        default = false;
-        defaultText = literalExpression "pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform";
-        type = types.bool;
-        description = lib.mdDoc ''
-          Validate the generated nginx config at build time. The check is not very robust and can be disabled in case of false positives. This is notably the case when cross-compiling or when using `include` with files outside of the store.
-        '';
-      };
-
       additionalModules = mkOption {
         default = [];
         type = types.listOf (types.attrsOf types.anything);
@@ -649,7 +713,7 @@ in
           Configuration lines appended to the generated Nginx
           configuration file. Commonly used by different modules
           providing http snippets. {option}`appendConfig`
-          can be specified more than once and it's value will be
+          can be specified more than once and its value will be
           concatenated (contrary to {option}`config` which
           can be set only once).
         '';
@@ -816,10 +880,10 @@ in
           '';
       };
 
-      proxyCache = mkOption {
-        type = types.submodule {
+      proxyCachePath = mkOption {
+        type = types.attrsOf (types.submodule ({ ... }: {
           options = {
-            enable = mkEnableOption (lib.mdDoc "Enable proxy cache");
+            enable = mkEnableOption (lib.mdDoc "this proxy cache path entry");
 
             keysZoneName = mkOption {
               type = types.str;
@@ -877,9 +941,12 @@ in
               description = lib.mdDoc "Set maximum cache size";
             };
           };
-        };
+        }));
         default = {};
-        description = lib.mdDoc "Configure proxy cache";
+        description = lib.mdDoc ''
+          Configure a proxy cache path entry.
+          See <http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_path> for documentation.
+        '';
       };
 
       resolver = mkOption {
@@ -922,6 +989,7 @@ in
           options = {
             servers = mkOption {
               type = types.attrsOf (types.submodule {
+                freeformType = types.attrsOf (types.oneOf [ types.bool types.int types.str ]);
                 options = {
                   backup = mkOption {
                     type = types.bool;
@@ -935,9 +1003,11 @@ in
               });
               description = lib.mdDoc ''
                 Defines the address and other parameters of the upstream servers.
+                See [the documentation](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#server)
+                for the available parameters.
               '';
               default = {};
-              example = { "127.0.0.1:8000" = {}; };
+              example = lib.literalMD "see [](#opt-services.nginx.upstreams)";
             };
             extraConfig = mkOption {
               type = types.lines;
@@ -952,14 +1022,23 @@ in
           Defines a group of servers to use as proxy target.
         '';
         default = {};
-        example = literalExpression ''
-          "backend_server" = {
-            servers = { "127.0.0.1:8000" = {}; };
-            extraConfig = ''''
+        example = {
+          "backend" = {
+            servers = {
+              "backend1.example.com:8080" = { weight = 5; };
+              "backend2.example.com" = { max_fails = 3; fail_timeout = "30s"; };
+              "backend3.example.com" = {};
+              "backup1.example.com" = { backup = true; };
+              "backup2.example.com" = { backup = true; };
+            };
+            extraConfig = ''
               keepalive 16;
-            '''';
+            '';
           };
-        '';
+          "memcached" = {
+            servers."unix:/run//memcached/memcached.sock" = {};
+          };
+        };
       };
 
       virtualHosts = mkOption {
@@ -990,11 +1069,15 @@ in
       The Nginx log directory has been moved to /var/log/nginx, the cache directory
       to /var/cache/nginx. The option services.nginx.stateDir has been removed.
     '')
+    (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "inactive" ] [ "services" "nginx" "proxyCachePath" "" "inactive" ])
+    (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "useTempPath" ] [ "services" "nginx" "proxyCachePath" "" "useTempPath" ])
+    (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "levels" ] [ "services" "nginx" "proxyCachePath" "" "levels" ])
+    (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "keysZoneSize" ] [ "services" "nginx" "proxyCachePath" "" "keysZoneSize" ])
+    (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "keysZoneName" ] [ "services" "nginx" "proxyCachePath" "" "keysZoneName" ])
+    (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "enable" ] [ "services" "nginx" "proxyCachePath" "" "enable" ])
   ];
 
   config = mkIf cfg.enable {
-    # TODO: test user supplied config file pases syntax test
-
     warnings =
     let
       deprecatedSSL = name: config: optional config.enableSSL
@@ -1049,13 +1132,48 @@ in
           services.nginx.virtualHosts.<name>.useACMEHost are mutually exclusive.
         '';
       }
+
+      {
+        assertion = cfg.package.pname != "nginxQuic" -> all (host: !host.quic) (attrValues virtualHosts);
+        message = ''
+          services.nginx.service.virtualHosts.<name>.quic requires using nginxQuic package,
+          which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`.
+        '';
+      }
+
+      {
+        # The idea is to understand whether there is a virtual host with a listen configuration
+        # that requires ACME configuration but has no HTTP listener which will make deterministically fail
+        # this operation.
+        # Options' priorities are the following at the moment:
+        # listen (vhost) > defaultListen (server) > listenAddresses (vhost) > defaultListenAddresses (server)
+        assertion =
+        let
+          hasAtLeastHttpListener = listenOptions: any (listenLine: if listenLine ? proxyProtocol then !listenLine.proxyProtocol else true) listenOptions;
+          hasAtLeastDefaultHttpListener = if cfg.defaultListen != [] then hasAtLeastHttpListener cfg.defaultListen else (cfg.defaultListenAddresses != []);
+        in
+          all (host:
+            let
+              hasAtLeastVhostHttpListener = if host.listen != [] then hasAtLeastHttpListener host.listen else (host.listenAddresses != []);
+              vhostAuthority = host.listen != [] || (cfg.defaultListen == [] && host.listenAddresses != []);
+            in
+              # Either vhost has precedence and we need a vhost specific http listener
+              # Either vhost set nothing and inherit from server settings
+              host.enableACME -> ((vhostAuthority && hasAtLeastVhostHttpListener) || (!vhostAuthority && hasAtLeastDefaultHttpListener))
+          ) (attrValues virtualHosts);
+        message = ''
+          services.nginx.virtualHosts.<name>.enableACME requires a HTTP listener
+          to answer to ACME requests.
+        '';
+      }
     ] ++ map (name: mkCertOwnershipAssertion {
       inherit (cfg) group user;
       cert = config.security.acme.certs.${name};
       groups = config.users.groups;
     }) dependentCertNames;
 
-    services.nginx.additionalModules = optional cfg.recommendedBrotliSettings pkgs.nginxModules.brotli;
+    services.nginx.additionalModules = optional cfg.recommendedBrotliSettings pkgs.nginxModules.brotli
+      ++ lib.optional cfg.recommendedZstdSettings pkgs.nginxModules.zstd;
 
     systemd.services.nginx = {
       description = "Nginx Web Server";
@@ -1130,7 +1248,7 @@ in
     };
 
     environment.etc."nginx/nginx.conf" = mkIf cfg.enableReload {
-      source = finalConfigFile;
+      source = configFile;
     };
 
     # This service waits for all certificates to be available
@@ -1142,14 +1260,14 @@ in
       sslServices = map (certName: "acme-${certName}.service") dependentCertNames;
       sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames;
     in mkIf (cfg.enableReload || sslServices != []) {
-      wants = optionals (cfg.enableReload) [ "nginx.service" ];
+      wants = optionals cfg.enableReload [ "nginx.service" ];
       wantedBy = sslServices ++ [ "multi-user.target" ];
       # Before the finished targets, after the renew services.
       # This service might be needed for HTTP-01 challenges, but we only want to confirm
       # certs are updated _after_ config has been reloaded.
       before = sslTargets;
       after = sslServices;
-      restartTriggers = optionals (cfg.enableReload) [ finalConfigFile ];
+      restartTriggers = optionals cfg.enableReload [ configFile ];
       # Block reloading if not all certs exist yet.
       # Happens when config changes add new vhosts/certs.
       unitConfig.ConditionPathExists = optionals (sslServices != []) (map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames);
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
index 089decb5f43..7636c1b2611 100644
--- a/nixos/modules/services/web-servers/nginx/vhost-options.nix
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -27,12 +27,35 @@ with lib;
     };
 
     listen = mkOption {
-      type = with types; listOf (submodule { options = {
-        addr = mkOption { type = str;  description = lib.mdDoc "IP address.";  };
-        port = mkOption { type = port;  description = lib.mdDoc "Port number."; default = 80; };
-        ssl  = mkOption { type = bool; description = lib.mdDoc "Enable SSL.";  default = false; };
-        extraParameters = mkOption { type = listOf str; description = lib.mdDoc "Extra parameters of this listen directive."; default = []; example = [ "backlog=1024" "deferred" ]; };
-      }; });
+      type = with types; listOf (submodule {
+        options = {
+          addr = mkOption {
+            type = str;
+            description = lib.mdDoc "IP address.";
+          };
+          port = mkOption {
+            type = port;
+            description = lib.mdDoc "Port number.";
+            default = 80;
+          };
+          ssl = mkOption {
+            type = bool;
+            description = lib.mdDoc "Enable SSL.";
+            default = false;
+          };
+          proxyProtocol = mkOption {
+            type = bool;
+            description = lib.mdDoc "Enable PROXY protocol.";
+            default = false;
+          };
+          extraParameters = mkOption {
+            type = listOf str;
+            description = lib.mdDoc "Extra parameters of this listen directive.";
+            default = [ ];
+            example = [ "backlog=1024" "deferred" ];
+          };
+        };
+      });
       default = [];
       example = [
         { addr = "195.154.1.1"; port = 443; ssl = true; }
@@ -45,7 +68,7 @@ with lib;
         and `onlySSL`.
 
         If you only want to set the addresses manually and not
-        the ports, take a look at `listenAddresses`
+        the ports, take a look at `listenAddresses`.
       '';
     };
 
@@ -188,11 +211,11 @@ with lib;
       type = types.bool;
       default = true;
       description = lib.mdDoc ''
-        Whether to enable HTTP 2.
+        Whether to enable the HTTP/2 protocol.
         Note that (as of writing) due to nginx's implementation, to disable
-        HTTP 2 you have to disable it on all vhosts that use a given
+        HTTP/2 you have to disable it on all vhosts that use a given
         IP address / port.
-        If there is one server block configured to enable http2,then it is
+        If there is one server block configured to enable http2, then it is
         enabled for all server blocks on this IP.
         See https://stackoverflow.com/a/39466948/263061.
       '';
@@ -200,12 +223,42 @@ with lib;
 
     http3 = mkOption {
       type = types.bool;
+      default = true;
+      description = lib.mdDoc ''
+        Whether to enable the HTTP/3 protocol.
+        This requires using `pkgs.nginxQuic` package
+        which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`
+        and activate the QUIC transport protocol
+        `services.nginx.virtualHosts.<name>.quic = true;`.
+        Note that HTTP/3 support is experimental and
+        *not* yet recommended for production.
+        Read more at https://quic.nginx.org/
+      '';
+    };
+
+    http3_hq = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Whether to enable the HTTP/0.9 protocol negotiation used in QUIC interoperability tests.
+        This requires using `pkgs.nginxQuic` package
+        which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`
+        and activate the QUIC transport protocol
+        `services.nginx.virtualHosts.<name>.quic = true;`.
+        Note that special application protocol support is experimental and
+        *not* yet recommended for production.
+        Read more at https://quic.nginx.org/
+      '';
+    };
+
+    quic = mkOption {
+      type = types.bool;
       default = false;
       description = lib.mdDoc ''
-        Whether to enable HTTP 3.
+        Whether to enable the QUIC transport protocol.
         This requires using `pkgs.nginxQuic` package
         which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`.
-        Note that HTTP 3 support is experimental and
+        Note that QUIC support is experimental and
         *not* yet recommended for production.
         Read more at https://quic.nginx.org/
       '';
diff --git a/nixos/modules/services/web-servers/rustus.nix b/nixos/modules/services/web-servers/rustus.nix
new file mode 100644
index 00000000000..95c9a645557
--- /dev/null
+++ b/nixos/modules/services/web-servers/rustus.nix
@@ -0,0 +1,252 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let
+  cfg = config.services.rustus;
+in
+{
+  meta.maintainers = with maintainers; [ happysalada ];
+
+  options.services.rustus = {
+
+    enable = mkEnableOption (lib.mdDoc "TUS protocol implementation in Rust.");
+
+    host = mkOption {
+      type = types.str;
+      description = lib.mdDoc ''
+        The host that rustus will connect to.
+      '';
+      default = "127.0.0.1";
+      example = "127.0.0.1";
+    };
+
+    port = mkOption {
+      type = types.port;
+      description = lib.mdDoc ''
+        The port that rustus will connect to.
+      '';
+      default = 1081;
+      example = 1081;
+    };
+
+    log_level = mkOption {
+      type = types.enum [ "DEBUG" "INFO" "ERROR" ];
+      description = lib.mdDoc ''
+        Desired log level
+      '';
+      default = "INFO";
+      example = "ERROR";
+    };
+
+    max_body_size = mkOption {
+      type = types.str;
+      description = lib.mdDoc ''
+        Maximum body size in bytes
+      '';
+      default = "10000000"; # 10 mb
+      example = "100000000";
+    };
+
+    url = mkOption {
+      type = types.str;
+      description = lib.mdDoc ''
+        url path for uploads
+      '';
+      default = "/files";
+    };
+
+    disable_health_access_logs = mkOption {
+      type = types.bool;
+      description = lib.mdDoc ''
+        disable access log for /health endpoint
+      '';
+      default = false;
+    };
+
+    cors = mkOption {
+      type = types.listOf types.str;
+      description = lib.mdDoc ''
+        list of origins allowed to upload
+      '';
+      default = ["*"];
+      example = ["*.staging.domain" "*.prod.domain"];
+    };
+
+    tus_extensions = mkOption {
+      type = types.listOf (types.enum [
+        "getting"
+        "creation"
+        "termination"
+        "creation-with-upload"
+        "creation-defer-length"
+        "concatenation"
+        "checksum"
+      ]);
+      description = lib.mdDoc ''
+        Since TUS protocol offers extensibility you can turn off some protocol extensions.
+      '';
+      default = [
+        "getting"
+        "creation"
+        "termination"
+        "creation-with-upload"
+        "creation-defer-length"
+        "concatenation"
+        "checksum"
+      ];
+    };
+
+    remove_parts = mkOption {
+      type = types.bool;
+      description = lib.mdDoc ''
+        remove parts files after successful concatenation
+      '';
+      default = true;
+      example = false;
+    };
+
+    storage = lib.mkOption {
+      description = lib.mdDoc ''
+        Storages are used to actually store your files. You can configure where you want to store files.
+      '';
+      default = {};
+      example = lib.literalExpression ''
+        {
+          type = "hybrid-s3"
+          s3_access_key_file = konfig.age.secrets.R2_ACCESS_KEY.path;
+          s3_secret_key_file = konfig.age.secrets.R2_SECRET_KEY.path;
+          s3_bucket = "my_bucket";
+          s3_url = "https://s3.example.com";
+        }
+      '';
+      type = lib.types.submodule {
+        options = {
+          type = lib.mkOption {
+            type = lib.types.enum ["file-storage" "hybrid-s3"];
+            description = lib.mdDoc "Type of storage to use";
+          };
+          s3_access_key_file = lib.mkOption {
+            type = lib.types.str;
+            description = lib.mdDoc "File path that contains the S3 access key.";
+          };
+          s3_secret_key_file = lib.mkOption {
+            type = lib.types.path;
+            description = lib.mdDoc "File path that contains the S3 secret key.";
+          };
+          s3_region = lib.mkOption {
+            type = lib.types.str;
+            default = "us-east-1";
+            description = lib.mdDoc "S3 region name.";
+          };
+          s3_bucket = lib.mkOption {
+            type = lib.types.str;
+            description = lib.mdDoc "S3 bucket.";
+          };
+          s3_url = lib.mkOption {
+            type = lib.types.str;
+            description = lib.mdDoc "S3 url.";
+          };
+
+          force_sync = lib.mkOption {
+            type = lib.types.bool;
+            description = lib.mdDoc "calls fsync system call after every write to disk in local storage";
+            default = true;
+          };
+          data_dir = lib.mkOption {
+            type = lib.types.str;
+            description = lib.mdDoc "path to the local directory where all files are stored";
+            default = "/var/lib/rustus";
+          };
+          dir_structure = lib.mkOption {
+            type = lib.types.str;
+            description = lib.mdDoc "pattern of a directory structure locally and on s3";
+            default = "{year}/{month}/{day}";
+          };
+        };
+      };
+    };
+
+    info_storage = lib.mkOption {
+      description = lib.mdDoc ''
+        Info storages are used to store information about file uploads. These storages must be persistent, because every time chunk is uploaded rustus updates information about upload. And when someone wants to download file, information about it requested from storage to get actual path of an upload.
+      '';
+      default = {};
+      type = lib.types.submodule {
+        options = {
+          type = lib.mkOption {
+            type = lib.types.enum ["file-info-storage"];
+            description = lib.mdDoc "Type of info storage to use";
+            default = "file-info-storage";
+          };
+          dir = lib.mkOption {
+            type = lib.types.str;
+            description = lib.mdDoc "directory to store info about uploads";
+            default = "/var/lib/rustus";
+          };
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    systemd.services.rustus =
+      let
+        isHybridS3 = cfg.storage.type == "hybrid-s3";
+      in
+    {
+      description = "Rustus server";
+      documentation = [ "https://s3rius.github.io/rustus/" ];
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      environment = {
+        RUSTUS_SERVER_HOST = cfg.host;
+        RUSTUS_SERVER_PORT = toString cfg.port;
+        RUSTUS_LOG_LEVEL = cfg.log_level;
+        RUSTUS_MAX_BODY_SIZE = cfg.max_body_size;
+        RUSTUS_URL = cfg.url;
+        RUSTUS_DISABLE_HEALTH_ACCESS_LOG = lib.mkIf cfg.disable_health_access_logs "true";
+        RUSTUS_CORS = lib.concatStringsSep "," cfg.cors;
+        RUSTUS_TUS_EXTENSIONS = lib.concatStringsSep "," cfg.tus_extensions;
+        RUSTUS_REMOVE_PARTS= if cfg.remove_parts then "true" else "false";
+        RUSTUS_STORAGE = cfg.storage.type;
+        RUSTUS_DATA_DIR = cfg.storage.data_dir;
+        RUSTUS_DIR_STRUCTURE = cfg.storage.dir_structure;
+        RUSTUS_FORCE_FSYNC = if cfg.storage.force_sync then "true" else "false";
+        RUSTUS_S3_URL = mkIf isHybridS3 cfg.storage.s3_url;
+        RUSTUS_S3_BUCKET = mkIf isHybridS3 cfg.storage.s3_bucket;
+        RUSTUS_S3_REGION = mkIf isHybridS3 cfg.storage.s3_region;
+        RUSTUS_S3_ACCESS_KEY_PATH = mkIf isHybridS3 "%d/S3_ACCESS_KEY_PATH";
+        RUSTUS_S3_SECRET_KEY_PATH = mkIf isHybridS3 "%d/S3_SECRET_KEY_PATH";
+        RUSTUS_INFO_STORAGE = cfg.info_storage.type;
+        RUSTUS_INFO_DIR = cfg.info_storage.dir;
+      };
+
+      serviceConfig = {
+        ExecStart = "${pkgs.rustus}/bin/rustus";
+        StateDirectory = "rustus";
+        DynamicUser = true;
+        LoadCredential = lib.optionals isHybridS3 [
+          "S3_ACCESS_KEY_PATH:${cfg.storage.s3_access_key_file}"
+          "S3_SECRET_KEY_PATH:${cfg.storage.s3_secret_key_file}"
+        ];
+        # hardening
+        RestrictRealtime=true;
+        RestrictNamespaces=true;
+        LockPersonality=true;
+        ProtectKernelModules=true;
+        ProtectKernelTunables=true;
+        ProtectKernelLogs=true;
+        ProtectControlGroups=true;
+        ProtectHostUserNamespaces=true;
+        ProtectClock=true;
+        RestrictSUIDSGID=true;
+        SystemCallArchitectures="native";
+        CapabilityBoundingSet="";
+        ProtectProc = "invisible";
+        # TODO consider SystemCallFilter LimitAS ProcSubset
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-servers/stargazer.nix b/nixos/modules/services/web-servers/stargazer.nix
new file mode 100644
index 00000000000..f0c3cf8787e
--- /dev/null
+++ b/nixos/modules/services/web-servers/stargazer.nix
@@ -0,0 +1,226 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.stargazer;
+  globalSection = ''
+    listen = ${lib.concatStringsSep " " cfg.listen}
+    connection-logging = ${lib.boolToString cfg.connectionLogging}
+    log-ip = ${lib.boolToString cfg.ipLog}
+    log-ip-partial = ${lib.boolToString cfg.ipLogPartial}
+    request-timeout = ${toString cfg.requestTimeout}
+    response-timeout = ${toString cfg.responseTimeout}
+
+    [:tls]
+    store = ${toString cfg.store}
+    organization = ${cfg.certOrg}
+    gen-certs = ${lib.boolToString cfg.genCerts}
+    regen-certs = ${lib.boolToString cfg.regenCerts}
+    ${lib.optionalString (cfg.certLifetime != "") "cert-lifetime = ${cfg.certLifetime}"}
+
+  '';
+  genINI = lib.generators.toINI { };
+  configFile = pkgs.writeText "config.ini" (lib.strings.concatStrings (
+    [ globalSection ] ++ (lib.lists.forEach cfg.routes (section:
+      let
+        name = section.route;
+        params = builtins.removeAttrs section [ "route" ];
+      in
+      genINI
+        {
+          "${name}" = params;
+        } + "\n"
+    ))
+  ));
+in
+{
+  options.services.stargazer = {
+    enable = lib.mkEnableOption (lib.mdDoc "Stargazer Gemini server");
+
+    listen = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
+      default = [ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]";
+      defaultText = lib.literalExpression ''[ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"'';
+      example = lib.literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]'';
+      description = lib.mdDoc ''
+        Address and port to listen on.
+      '';
+    };
+
+    connectionLogging = lib.mkOption {
+      type = lib.types.bool;
+      default = true;
+      description = lib.mdDoc "Whether or not to log connections to stdout.";
+    };
+
+    ipLog = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = lib.mdDoc "Log client IP addresses in the connection log.";
+    };
+
+    ipLogPartial = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = lib.mdDoc "Log partial client IP addresses in the connection log.";
+    };
+
+    requestTimeout = lib.mkOption {
+      type = lib.types.int;
+      default = 5;
+      description = lib.mdDoc ''
+        Number of seconds to wait for the client to send a complete
+        request. Set to 0 to disable.
+      '';
+    };
+
+    responseTimeout = lib.mkOption {
+      type = lib.types.int;
+      default = 0;
+      description = lib.mdDoc ''
+        Number of seconds to wait for the client to send a complete
+        request and for stargazer to finish sending the response.
+        Set to 0 to disable.
+      '';
+    };
+
+    store = lib.mkOption {
+      type = lib.types.path;
+      default = /var/lib/gemini/certs;
+      description = lib.mdDoc ''
+        Path to the certificate store on disk. This should be a
+        persistent directory writable by Stargazer.
+      '';
+    };
+
+    certOrg = lib.mkOption {
+      type = lib.types.str;
+      default = "stargazer";
+      description = lib.mdDoc ''
+        The name of the organization responsible for the X.509
+        certificate's /O name.
+      '';
+    };
+
+    genCerts = lib.mkOption {
+      type = lib.types.bool;
+      default = true;
+      description = lib.mdDoc ''
+        Set to false to disable automatic certificate generation.
+        Use if you want to provide your own certs.
+      '';
+    };
+
+    regenCerts = lib.mkOption {
+      type = lib.types.bool;
+      default = true;
+      description = lib.mdDoc ''
+        Set to false to turn off automatic regeneration of expired certificates.
+        Use if you want to provide your own certs.
+      '';
+    };
+
+    certLifetime = lib.mkOption {
+      type = lib.types.str;
+      default = "";
+      description = lib.mdDoc ''
+        How long certs generated by Stargazer should live for.
+        Certs live forever by default.
+      '';
+      example = lib.literalExpression "\"1y\"";
+    };
+
+    routes = lib.mkOption {
+      type = lib.types.listOf
+        (lib.types.submodule {
+          freeformType = with lib.types; attrsOf (nullOr
+            (oneOf [
+              bool
+              int
+              float
+              str
+            ]) // {
+            description = "INI atom (null, bool, int, float or string)";
+          });
+          options.route = lib.mkOption {
+            type = lib.types.str;
+            description = lib.mdDoc "Route section name";
+          };
+        });
+      default = [ ];
+      description = lib.mdDoc ''
+        Routes that Stargazer should server.
+
+        Expressed as a list of attribute sets. Each set must have a key `route`
+        that becomes the section name for that route in the stargazer ini cofig.
+        The remaining keys and values become the parameters for that route.
+
+        [Refer to upstream docs for other params](https://git.sr.ht/~zethra/stargazer/tree/main/item/doc/stargazer.ini.5.txt)
+      '';
+      example = lib.literalExpression ''
+        [
+          {
+            route = "example.com";
+            root = "/srv/gemini/example.com"
+          }
+          {
+            route = "example.com:/man";
+            root = "/cgi-bin";
+            cgi = true;
+          }
+          {
+            route = "other.org~(.*)";
+            redirect = "gemini://example.com";
+            rewrite = "\1";
+          }
+        ]
+      '';
+    };
+
+    user = lib.mkOption {
+      type = lib.types.str;
+      default = "stargazer";
+      description = lib.mdDoc "User account under which stargazer runs.";
+    };
+
+    group = lib.mkOption {
+      type = lib.types.str;
+      default = "stargazer";
+      description = lib.mdDoc "Group account under which stargazer runs.";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.stargazer = {
+      description = "Stargazer gemini server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.stargazer}/bin/stargazer ${configFile}";
+        Restart = "always";
+        # User and group
+        User = cfg.user;
+        Group = cfg.group;
+      };
+    };
+
+    # Create default cert store
+    system.activationScripts.makeStargazerCertDir =
+      lib.optionalAttrs (cfg.store == /var/lib/gemini/certs) ''
+        mkdir -p /var/lib/gemini/certs
+        chown -R ${cfg.user}:${cfg.group} /var/lib/gemini/certs
+      '';
+
+    users.users = lib.optionalAttrs (cfg.user == "stargazer") {
+      stargazer = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = lib.optionalAttrs (cfg.group == "stargazer") {
+      stargazer = { };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ gaykitty ];
+}
diff --git a/nixos/modules/services/web-servers/static-web-server.nix b/nixos/modules/services/web-servers/static-web-server.nix
new file mode 100644
index 00000000000..07187f00fec
--- /dev/null
+++ b/nixos/modules/services/web-servers/static-web-server.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.static-web-server;
+  toml = pkgs.formats.toml {};
+  configFilePath = toml.generate "config.toml" cfg.configuration;
+in {
+  options = {
+    services.static-web-server = {
+      enable = lib.mkEnableOption (lib.mdDoc ''Static Web Server'');
+      listen = lib.mkOption {
+        default = "[::]:8787";
+        type = lib.types.str;
+        description = lib.mdDoc ''
+          The "ListenStream" used in static-web-server.socket.
+          This is equivalent to SWS's "host" and "port" options.
+          See here for specific syntax: <https://www.freedesktop.org/software/systemd/man/systemd.socket.html#ListenStream=>
+        '';
+      };
+      root = lib.mkOption {
+        type = lib.types.path;
+        description = lib.mdDoc ''
+          The location of files for SWS to serve. Equivalent to SWS's "root" config value.
+          NOTE: This folder must exist before starting SWS.
+        '';
+      };
+      configuration = lib.mkOption {
+        default = { };
+        type = toml.type;
+        example = {
+          general = { log-level = "error"; directory-listing = true; };
+        };
+        description = lib.mdDoc ''
+          Configuration for Static Web Server. See
+          <https://static-web-server.net/configuration/config-file/>.
+          NOTE: Don't set "host", "port", or "root" here. They will be ignored.
+          Use the top-level "listen" and "root" options instead.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.static-web-server ];
+    systemd.packages = [ pkgs.static-web-server ];
+    # Have to set wantedBy since systemd.packages ignores the "Install" section
+    systemd.sockets.static-web-server = {
+      wantedBy = [ "sockets.target" ];
+      # Start with empty string to reset upstream option
+      listenStreams = [ "" cfg.listen ];
+    };
+    systemd.services.static-web-server = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        # Remove upstream sample environment file; use config.toml exclusively
+        EnvironmentFile = [ "" ];
+        ExecStart = [ "" "${pkgs.static-web-server}/bin/static-web-server --fd 0 --config-file ${configFilePath} --root ${cfg.root}" ];
+        # Supplementary groups doesn't work unless we create the group ourselves
+        SupplementaryGroups = [ "" ];
+        # If the user is serving files from their home dir, override ProtectHome to allow that
+        ProtectHome = if lib.hasPrefix "/home" cfg.root then "tmpfs" else "true";
+        BindReadOnlyPaths = cfg.root;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ mac-chaffee ];
+}
diff --git a/nixos/modules/services/web-servers/tomcat.nix b/nixos/modules/services/web-servers/tomcat.nix
index d8bfee547c7..4d2c36287be 100644
--- a/nixos/modules/services/web-servers/tomcat.nix
+++ b/nixos/modules/services/web-servers/tomcat.nix
@@ -234,11 +234,11 @@ in
           ln -sfn ${tomcat}/conf/$i ${cfg.baseDir}/conf/`basename $i`
         done
 
-        ${if cfg.extraConfigFiles != [] then ''
+        ${optionalString (cfg.extraConfigFiles != []) ''
           for i in ${toString cfg.extraConfigFiles}; do
             ln -sfn $i ${cfg.baseDir}/conf/`basename $i`
           done
-        '' else ""}
+        ''}
 
         # Create a modified catalina.properties file
         # Change all references from CATALINA_HOME to CATALINA_BASE and add support for shared libraries
@@ -345,7 +345,7 @@ in
 
           # Symlink all the given web applications files or paths into the webapps/ directory
           # of this virtual host
-          for i in "${if virtualHost ? webapps then toString virtualHost.webapps else ""}"; do
+          for i in "${optionalString (virtualHost ? webapps) (toString virtualHost.webapps)}"; do
             if [ -f $i ]; then
               # If the given web application is a file, symlink it into the webapps/ directory
               ln -sfn $i ${cfg.baseDir}/virtualhosts/${virtualHost.name}/webapps/`basename $i`
diff --git a/nixos/modules/services/web-servers/traefik.nix b/nixos/modules/services/web-servers/traefik.nix
index 9e5603e0edc..42fb95a5220 100644
--- a/nixos/modules/services/web-servers/traefik.nix
+++ b/nixos/modules/services/web-servers/traefik.nix
@@ -48,6 +48,11 @@ let
     ''
   else
     cfg.staticConfigFile;
+
+  finalStaticConfigFile =
+    if cfg.environmentFiles == []
+    then staticConfigFile
+    else "/run/traefik/config.toml";
 in {
   options.services.traefik = {
     enable = mkEnableOption (lib.mdDoc "Traefik web server");
@@ -127,6 +132,16 @@ in {
       type = types.package;
       description = lib.mdDoc "Traefik package to use.";
     };
+
+    environmentFiles = mkOption {
+      default = [];
+      type = types.listOf types.path;
+      example = [ "/run/secrets/traefik.env" ];
+      description = lib.mdDoc ''
+        Files to load as environment file. Environment variables from this file
+        will be substituted into the static configuration file using envsubst.
+      '';
+    };
   };
 
   config = mkIf cfg.enable {
@@ -139,8 +154,13 @@ in {
       startLimitIntervalSec = 86400;
       startLimitBurst = 5;
       serviceConfig = {
-        ExecStart =
-          "${cfg.package}/bin/traefik --configfile=${staticConfigFile}";
+        EnvironmentFile = cfg.environmentFiles;
+        ExecStartPre = lib.optional (cfg.environmentFiles != [])
+          (pkgs.writeShellScript "pre-start" ''
+            umask 077
+            ${pkgs.envsubst}/bin/envsubst -i "${staticConfigFile}" > "${finalStaticConfigFile}"
+          '');
+        ExecStart = "${cfg.package}/bin/traefik --configfile=${finalStaticConfigFile}";
         Type = "simple";
         User = "traefik";
         Group = cfg.group;
@@ -155,6 +175,7 @@ in {
         ProtectHome = true;
         ProtectSystem = "full";
         ReadWriteDirectories = cfg.dataDir;
+        RuntimeDirectory = "traefik";
       };
     };
 
diff --git a/nixos/modules/services/web-servers/ttyd.nix b/nixos/modules/services/web-servers/ttyd.nix
index e0a8b5179e0..3b1d87ccb48 100644
--- a/nixos/modules/services/web-servers/ttyd.nix
+++ b/nixos/modules/services/web-servers/ttyd.nix
@@ -78,11 +78,12 @@ in
       clientOptions = mkOption {
         type = types.attrsOf types.str;
         default = {};
-        example = literalExpression ''{
-          fontSize = "16";
-          fontFamily = "Fira Code";
-
-        }'';
+        example = literalExpression ''
+          {
+            fontSize = "16";
+            fontFamily = "Fira Code";
+          }
+        '';
         description = lib.mdDoc ''
           Attribute set of client options for xtermjs.
           <https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/>
diff --git a/nixos/modules/services/web-servers/unit/default.nix b/nixos/modules/services/web-servers/unit/default.nix
index 0aaac8a14e4..1515779c906 100644
--- a/nixos/modules/services/web-servers/unit/default.nix
+++ b/nixos/modules/services/web-servers/unit/default.nix
@@ -104,7 +104,7 @@ in {
         PIDFile = "/run/unit/unit.pid";
         ExecStart = ''
           ${cfg.package}/bin/unitd --control 'unix:/run/unit/control.unit.sock' --pid '/run/unit/unit.pid' \
-                                   --log '${cfg.logDir}/unit.log' --state '${cfg.stateDir}' --tmp '/tmp' \
+                                   --log '${cfg.logDir}/unit.log' --statedir '${cfg.stateDir}' --tmpdir '/tmp' \
                                    --user ${cfg.user} --group ${cfg.group}
         '';
         ExecStop = ''
diff --git a/nixos/modules/services/web-servers/varnish/default.nix b/nixos/modules/services/web-servers/varnish/default.nix
index e34c22d2868..d7f19be0cec 100644
--- a/nixos/modules/services/web-servers/varnish/default.nix
+++ b/nixos/modules/services/web-servers/varnish/default.nix
@@ -99,7 +99,7 @@ in
     environment.systemPackages = [ cfg.package ];
 
     # check .vcl syntax at compile time (e.g. before nixops deployment)
-    system.extraDependencies = mkIf cfg.enableConfigCheck [
+    system.checks = mkIf cfg.enableConfigCheck [
       (pkgs.runCommand "check-varnish-syntax" {} ''
         ${cfg.package}/bin/varnishd -C ${commandLine} 2> $out || (cat $out; exit 1)
       '')
diff --git a/nixos/modules/services/x11/desktop-managers/budgie.nix b/nixos/modules/services/x11/desktop-managers/budgie.nix
new file mode 100644
index 00000000000..b7341d4d8b4
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/budgie.nix
@@ -0,0 +1,244 @@
+{ lib, pkgs, config, utils, ... }:
+
+let
+  inherit (lib) concatMapStrings literalExpression mdDoc mkDefault mkEnableOption mkIf mkOption types;
+
+  cfg = config.services.xserver.desktopManager.budgie;
+
+  nixos-background-light = pkgs.nixos-artwork.wallpapers.nineish;
+  nixos-background-dark = pkgs.nixos-artwork.wallpapers.nineish-dark-gray;
+
+  nixos-gsettings-overrides = pkgs.budgie.budgie-gsettings-overrides.override {
+    inherit (cfg) extraGSettingsOverrides extraGSettingsOverridePackages;
+    inherit nixos-background-dark nixos-background-light;
+  };
+
+  nixos-background-info = pkgs.writeTextFile {
+    name = "nixos-background-info";
+    text = ''
+      <?xml version="1.0"?>
+      <!DOCTYPE wallpapers SYSTEM "gnome-wp-list.dtd">
+      <wallpapers>
+        <wallpaper deleted="false">
+          <name>Nineish</name>
+          <filename>${nixos-background-light.gnomeFilePath}</filename>
+          <options>zoom</options>
+          <shade_type>solid</shade_type>
+          <pcolor>#d1dcf8</pcolor>
+          <scolor>#e3ebfe</scolor>
+        </wallpaper>
+        <wallpaper deleted="false">
+          <name>Nineish Dark Gray</name>
+          <filename>${nixos-background-dark.gnomeFilePath}</filename>
+          <options>zoom</options>
+          <shade_type>solid</shade_type>
+          <pcolor>#151515</pcolor>
+          <scolor>#262626</scolor>
+        </wallpaper>
+      </wallpapers>
+    '';
+    destination = "/share/gnome-background-properties/nixos.xml";
+  };
+in {
+  options = {
+    services.xserver.desktopManager.budgie = {
+      enable = mkEnableOption (mdDoc "the Budgie desktop");
+
+      sessionPath = mkOption {
+        description = lib.mdDoc ''
+          Additional list of packages to be added to the session search path.
+          Useful for GSettings-conditional autostart.
+
+          Note that this should be a last resort; patching the package is preferred (see GPaste).
+        '';
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.gnome.gpaste ]";
+      };
+
+      extraGSettingsOverrides = mkOption {
+        description = mdDoc "Additional GSettings overrides.";
+        type = types.lines;
+        default = "";
+      };
+
+      extraGSettingsOverridePackages = mkOption {
+        description = mdDoc "List of packages for which GSettings are overridden.";
+        type = types.listOf types.path;
+        default = [];
+      };
+
+      extraPlugins = mkOption {
+        description = mdDoc "Extra plugins for the Budgie desktop";
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.budgiePlugins.budgie-analogue-clock-applet ]";
+      };
+    };
+
+    environment.budgie.excludePackages = mkOption {
+      description = mdDoc "Which packages Budgie should exclude from the default environment.";
+      type = types.listOf types.package;
+      default = [];
+      example = literalExpression "[ pkgs.mate-terminal ]";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.displayManager.sessionPackages = with pkgs; [
+      budgie.budgie-desktop
+    ];
+
+    services.xserver.displayManager.lightdm.greeters.slick = {
+      enable = mkDefault true;
+      theme = mkDefault { name = "Qogir"; package = pkgs.qogir-theme; };
+      iconTheme = mkDefault { name = "Qogir"; package = pkgs.qogir-icon-theme; };
+      cursorTheme = mkDefault { name = "Qogir"; package = pkgs.qogir-icon-theme; };
+    };
+
+    services.xserver.desktopManager.budgie.sessionPath = [ pkgs.budgie.budgie-desktop-view ];
+
+    environment.extraInit = ''
+      ${concatMapStrings (p: ''
+        if [ -d "${p}/share/gsettings-schemas/${p.name}" ]; then
+          export XDG_DATA_DIRS=$XDG_DATA_DIRS''${XDG_DATA_DIRS:+:}${p}/share/gsettings-schemas/${p.name}
+        fi
+        if [ -d "${p}/lib/girepository-1.0" ]; then
+          export GI_TYPELIB_PATH=$GI_TYPELIB_PATH''${GI_TYPELIB_PATH:+:}${p}/lib/girepository-1.0
+          export LD_LIBRARY_PATH=$LD_LIBRARY_PATH''${LD_LIBRARY_PATH:+:}${p}/lib
+        fi
+      '') cfg.sessionPath}
+    '';
+
+    environment.systemPackages = with pkgs;
+      [
+        # Budgie Desktop.
+        budgie.budgie-backgrounds
+        budgie.budgie-control-center
+        (budgie.budgie-desktop-with-plugins.override { plugins = cfg.extraPlugins; })
+        budgie.budgie-desktop-view
+        budgie.budgie-screensaver
+
+        # Required by the Budgie Desktop session.
+        (gnome.gnome-session.override { gnomeShellSupport = false; })
+
+        # Required by Budgie Menu.
+        gnome-menus
+
+        # Required by Budgie Control Center.
+        gnome.zenity
+
+        # Provides `gsettings`.
+        glib
+
+        # Update user directories.
+        xdg-user-dirs
+      ]
+      ++ (utils.removePackagesByName [
+          cinnamon.nemo
+          mate.eom
+          mate.pluma
+          mate.atril
+          mate.engrampa
+          mate.mate-calc
+          mate.mate-terminal
+          mate.mate-system-monitor
+          vlc
+
+          # Desktop themes.
+          qogir-theme
+          qogir-icon-theme
+          nixos-background-info
+
+          # Default settings.
+          nixos-gsettings-overrides
+        ] config.environment.budgie.excludePackages)
+      ++ cfg.sessionPath;
+
+    # Fonts.
+    fonts.fonts = mkDefault [
+      pkgs.noto-fonts
+      pkgs.hack-font
+    ];
+    fonts.fontconfig.defaultFonts = {
+      sansSerif = mkDefault ["Noto Sans"];
+      monospace = mkDefault ["Hack"];
+    };
+
+    # Qt application style.
+    qt = {
+      enable = mkDefault true;
+      style = mkDefault "gtk2";
+      platformTheme = mkDefault "gtk2";
+    };
+
+    environment.pathsToLink = [
+      "/share" # TODO: https://github.com/NixOS/nixpkgs/issues/47173
+    ];
+
+    # GSettings overrides.
+    environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-overrides}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
+
+    # Required by Budgie Desktop.
+    services.xserver.updateDbusEnvironment = true;
+    programs.dconf.enable = true;
+
+    # Required by Budgie Screensaver.
+    security.pam.services.budgie-screensaver = {};
+
+    # Required by Budgie's Polkit Dialog.
+    security.polkit.enable = mkDefault true;
+
+    # Required by Budgie Panel plugins and/or Budgie Control Center panels.
+    networking.networkmanager.enable = mkDefault true; # for BCC's Network panel.
+    programs.nm-applet.enable = config.networking.networkmanager.enable; # Budgie has no Network applet.
+    programs.nm-applet.indicator = false; # Budgie doesn't support AppIndicators.
+
+    hardware.bluetooth.enable = mkDefault true; # for Budgie's Status Indicator and BCC's Bluetooth panel.
+    hardware.pulseaudio.enable = mkDefault true; # for Budgie's Status Indicator and BCC's Sound panel.
+
+    xdg.portal.enable = mkDefault true; # for BCC's Applications panel.
+    xdg.portal.extraPortals = with pkgs; [
+      xdg-desktop-portal-gtk # provides a XDG Portals implementation.
+    ];
+
+    services.geoclue2.enable = mkDefault true; # for BCC's Privacy > Location Services panel.
+    services.upower.enable = config.powerManagement.enable; # for Budgie's Status Indicator and BCC's Power panel.
+    services.xserver.libinput.enable = mkDefault true; # for BCC's Mouse panel.
+    services.colord.enable = mkDefault true; # for BCC's Color panel.
+    services.gnome.at-spi2-core.enable = mkDefault true; # for BCC's A11y panel.
+    services.accounts-daemon.enable = mkDefault true; # for BCC's Users panel.
+    services.fprintd.enable = mkDefault true; # for BCC's Users panel.
+    services.udisks2.enable = mkDefault true; # for BCC's Details panel.
+
+    # For BCC's Online Accounts panel.
+    services.gnome.gnome-online-accounts.enable = mkDefault true;
+    services.gnome.gnome-online-miners.enable = true;
+
+    # For BCC's Printers panel.
+    services.printing.enable = mkDefault true;
+    services.system-config-printer.enable = config.services.printing.enable;
+
+    # For BCC's Sharing panel.
+    services.dleyna-renderer.enable = mkDefault true;
+    services.dleyna-server.enable = mkDefault true;
+    services.gnome.gnome-user-share.enable = mkDefault true;
+    services.gnome.rygel.enable = mkDefault true;
+
+    # Other default services.
+    services.gnome.evolution-data-server.enable = mkDefault true;
+    services.gnome.glib-networking.enable = mkDefault true;
+    services.gnome.gnome-keyring.enable = mkDefault true;
+    services.gnome.gnome-settings-daemon.enable = mkDefault true;
+    services.gvfs.enable = mkDefault true;
+
+    # Register packages for DBus.
+    services.dbus.packages = with pkgs; [
+      budgie.budgie-control-center
+    ];
+
+    # Shell integration for MATE Terminal.
+    programs.bash.vteIntegration = true;
+    programs.zsh.vteIntegration = true;
+  };
+}
diff --git a/nixos/modules/services/x11/desktop-managers/cde.nix b/nixos/modules/services/x11/desktop-managers/cde.nix
index e0b4fb0e7bf..ad4b5d27f9d 100644
--- a/nixos/modules/services/x11/desktop-managers/cde.nix
+++ b/nixos/modules/services/x11/desktop-managers/cde.nix
@@ -36,7 +36,7 @@ in {
         name = "cmsd";
         protocol = "udp";
         user = "root";
-        server = "${pkgs.cdesktopenv}/opt/dt/bin/rpc.cmsd";
+        server = "${pkgs.cdesktopenv}/bin/rpc.cmsd";
         extraConfig = ''
           type  = RPC UNLISTED
           rpc_number  = 100068
@@ -64,7 +64,7 @@ in {
     services.xserver.desktopManager.session = [
     { name = "CDE";
       start = ''
-        exec ${pkgs.cdesktopenv}/opt/dt/bin/Xsession
+        exec ${pkgs.cdesktopenv}/bin/Xsession
       '';
     }];
   };
diff --git a/nixos/modules/services/x11/desktop-managers/cinnamon.nix b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
index 08c5625fc7d..c7755aca4bb 100644
--- a/nixos/modules/services/x11/desktop-managers/cinnamon.nix
+++ b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
@@ -70,9 +70,9 @@ in
           name = mkDefault "Mint-Y-Aqua";
           package = mkDefault pkgs.cinnamon.mint-themes;
         };
-        iconTheme = mkIf (notExcluded pkgs.cinnamon.mint-x-icons) {
-          name = mkDefault "Mint-Y-Aqua";
-          package = mkDefault pkgs.cinnamon.mint-x-icons;
+        iconTheme = mkIf (notExcluded pkgs.cinnamon.mint-y-icons) {
+          name = mkDefault "Mint-Y-Sand";
+          package = mkDefault pkgs.cinnamon.mint-y-icons;
         };
         cursorTheme = mkIf (notExcluded pkgs.cinnamon.mint-cursor-themes) {
           name = mkDefault "Bibata-Modern-Classic";
@@ -109,9 +109,12 @@ in
         xapp
       ];
       services.cinnamon.apps.enable = mkDefault true;
+      services.gnome.evolution-data-server.enable = true;
       services.gnome.glib-networking.enable = true;
       services.gnome.gnome-keyring.enable = true;
       services.gvfs.enable = true;
+      services.switcherooControl.enable = mkDefault true; # xapp-gpu-offload-helper
+      services.touchegg.enable = mkDefault true;
       services.udisks2.enable = true;
       services.upower.enable = mkDefault config.powerManagement.enable;
       services.xserver.libinput.enable = mkDefault true;
@@ -177,14 +180,26 @@ in
         nixos-artwork.wallpapers.simple-dark-gray
         mint-artwork
         mint-cursor-themes
+        mint-l-icons
+        mint-l-theme
         mint-themes
         mint-x-icons
         mint-y-icons
+        xapp # provides some xapp-* icons
       ] config.environment.cinnamon.excludePackages);
 
       xdg.mime.enable = true;
       xdg.icons.enable = true;
 
+      xdg.portal.enable = true;
+      xdg.portal.extraPortals = [
+        pkgs.xdg-desktop-portal-xapp
+        (pkgs.xdg-desktop-portal-gtk.override {
+          # Do not build portals that we already have.
+          buildPortalsInGnome = false;
+        })
+      ];
+
       # Override GSettings schemas
       environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-overrides}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
 
@@ -197,10 +212,10 @@ in
       programs.bash.vteIntegration = mkDefault true;
       programs.zsh.vteIntegration = mkDefault true;
 
-      # Harmonize Qt5 applications under Cinnamon
-      qt5.enable = true;
-      qt5.platformTheme = "gnome";
-      qt5.style = "adwaita";
+      # Harmonize Qt applications under Cinnamon
+      qt.enable = true;
+      qt.platformTheme = "gnome";
+      qt.style = "adwaita";
 
       # Default Fonts
       fonts.fonts = with pkgs; [
@@ -213,7 +228,6 @@ in
       programs.geary.enable = mkDefault true;
       programs.gnome-disks.enable = mkDefault true;
       programs.gnome-terminal.enable = mkDefault true;
-      programs.evince.enable = mkDefault true;
       programs.file-roller.enable = mkDefault true;
 
       environment.systemPackages = with pkgs // pkgs.gnome // pkgs.cinnamon; utils.removePackagesByName [
@@ -231,6 +245,7 @@ in
         # external apps shipped with linux-mint
         hexchat
         gnome-calculator
+        gnome-calendar
         gnome-screenshot
       ] config.environment.cinnamon.excludePackages;
     })
diff --git a/nixos/modules/services/x11/desktop-managers/deepin.nix b/nixos/modules/services/x11/desktop-managers/deepin.nix
new file mode 100644
index 00000000000..70be6ed7c05
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/deepin.nix
@@ -0,0 +1,208 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+  xcfg = config.services.xserver;
+  cfg = xcfg.desktopManager.deepin;
+
+  nixos-gsettings-overrides = pkgs.deepin.dde-gsettings-schemas.override {
+    extraGSettingsOverridePackages = cfg.extraGSettingsOverridePackages;
+    extraGSettingsOverrides = cfg.extraGSettingsOverrides;
+  };
+in
+{
+  options = {
+
+    services.xserver.desktopManager.deepin = {
+      enable = mkEnableOption (lib.mdDoc "Enable Deepin desktop manager");
+      extraGSettingsOverrides = mkOption {
+        default = "";
+        type = types.lines;
+        description = lib.mdDoc "Additional gsettings overrides.";
+      };
+      extraGSettingsOverridePackages = mkOption {
+        default = [ ];
+        type = types.listOf types.path;
+        description = lib.mdDoc "List of packages for which gsettings are overridden.";
+      };
+    };
+
+    environment.deepin.excludePackages = mkOption {
+      default = [ ];
+      type = types.listOf types.package;
+      description = lib.mdDoc "List of default packages to exclude from the configuration";
+    };
+
+  };
+
+  config = mkIf cfg.enable
+    {
+      services.xserver.displayManager.sessionPackages = [ pkgs.deepin.startdde ];
+      services.xserver.displayManager.defaultSession = mkDefault "deepin";
+
+      # Update the DBus activation environment after launching the desktop manager.
+      services.xserver.displayManager.sessionCommands = ''
+        ${lib.getBin pkgs.dbus}/bin/dbus-update-activation-environment --systemd --all
+      '';
+
+      hardware.bluetooth.enable = mkDefault true;
+      hardware.pulseaudio.enable = mkDefault true;
+      security.polkit.enable = true;
+
+      services.deepin.dde-daemon.enable = mkForce true;
+      services.deepin.dde-api.enable = mkForce true;
+      services.deepin.app-services.enable = mkForce true;
+
+      services.colord.enable = mkDefault true;
+      services.accounts-daemon.enable = mkDefault true;
+      services.gvfs.enable = mkDefault true;
+      services.gnome.glib-networking.enable = mkDefault true;
+      services.gnome.gnome-keyring.enable = mkDefault true;
+      services.bamf.enable = mkDefault true;
+
+      services.xserver.libinput.enable = mkDefault true;
+      services.udisks2.enable = true;
+      services.upower.enable = mkDefault config.powerManagement.enable;
+      networking.networkmanager.enable = mkDefault true;
+      programs.dconf.enable = mkDefault true;
+
+      fonts.fonts = with pkgs; [ noto-fonts ];
+      xdg.mime.enable = true;
+      xdg.menus.enable = true;
+      xdg.icons.enable = true;
+      xdg.portal.enable = mkDefault true;
+      xdg.portal.extraPortals = mkDefault [
+        (pkgs.xdg-desktop-portal-gtk.override {
+          buildPortalsInGnome = false;
+        })
+      ];
+
+      environment.sessionVariables = {
+        NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-overrides}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
+        DDE_POLKIT_AGENT_PLUGINS_DIRS = [ "${pkgs.deepin.dpa-ext-gnomekeyring}/lib/polkit-1-dde/plugins" ];
+      };
+
+      environment.pathsToLink = [
+        "/lib/dde-dock/plugins"
+        "/lib/dde-control-center"
+        "/lib/dde-session-shell"
+        "/lib/dde-file-manager"
+        "/share/backgrounds"
+        "/share/wallpapers"
+      ];
+
+      environment.etc = {
+        "distribution.info".text = ''
+          [Distribution]
+          Name=NixOS
+          WebsiteName=www.nixos.org
+          Website=https://www.nixos.org
+          Logo=${pkgs.nixos-icons}/share/icons/hicolor/96x96/apps/nix-snowflake.png
+          LogoLight=${pkgs.nixos-icons}/share/icons/hicolor/32x32/apps/nix-snowflake.png
+          LogoTransparent=${pkgs.deepin.deepin-desktop-base}/share/pixmaps/distribution_logo_transparent.svg
+        '';
+        "deepin-installer.conf".text = ''
+          system_info_vendor_name="Copyright (c) 2003-2023 NixOS contributors"
+        '';
+      };
+
+      systemd.tmpfiles.rules = [
+        "d /var/lib/AccountsService 0775 root root - -"
+        "C /var/lib/AccountsService/icons 0775 root root - ${pkgs.deepin.dde-account-faces}/var/lib/AccountsService/icons"
+      ];
+
+      security.pam.services.dde-lock.text = ''
+        # original at {dde-session-shell}/etc/pam.d/dde-lock
+        auth      substack      login
+        account   include       login
+        password  substack      login
+        session   include       login
+      '';
+
+      environment.systemPackages = with pkgs; with deepin;
+        let
+          requiredPackages = [
+            pciutils # for dtkcore/startdde
+            xdotool # for dde-daemon
+            glib # for gsettings program / gdbus
+            gtk3 # for gtk-launch program
+            xdg-user-dirs # Update user dirs
+            util-linux # runuser
+            polkit_gnome
+            librsvg # dde-api use rsvg-convert
+            lshw # for dtkcore
+            libsForQt5.kde-gtk-config # deepin-api/gtk-thumbnailer need
+            libsForQt5.kglobalaccel
+            xsettingsd # lightdm-deepin-greeter
+            qt5platform-plugins
+            deepin-pw-check
+            deepin-turbo
+
+            dde-account-faces
+            deepin-icon-theme
+            deepin-sound-theme
+            deepin-gtk-theme
+            deepin-wallpapers
+
+            startdde
+            dde-dock
+            dde-launcher
+            dde-session-ui
+            dde-session-shell
+            dde-file-manager
+            dde-control-center
+            dde-network-core
+            dde-clipboard
+            dde-calendar
+            dde-polkit-agent
+            dpa-ext-gnomekeyring
+            deepin-desktop-schemas
+            deepin-terminal
+            dde-kwin
+            deepin-kwin
+          ];
+          optionalPackages = [
+            onboard # dde-dock plugin
+            deepin-camera
+            deepin-calculator
+            deepin-compressor
+            deepin-editor
+            deepin-picker
+            deepin-draw
+            deepin-album
+            deepin-image-viewer
+            deepin-music
+            deepin-movie-reborn
+            deepin-system-monitor
+            deepin-screen-recorder
+            deepin-shortcut-viewer
+          ];
+        in
+        requiredPackages
+        ++ utils.removePackagesByName optionalPackages config.environment.deepin.excludePackages;
+
+      services.dbus.packages = with pkgs.deepin; [
+        dde-dock
+        dde-launcher
+        dde-session-ui
+        dde-session-shell
+        dde-file-manager
+        dde-control-center
+        dde-calendar
+        dde-clipboard
+        dde-kwin
+        deepin-kwin
+        deepin-pw-check
+      ];
+
+      systemd.packages = with pkgs.deepin; [
+        dde-launcher
+        dde-file-manager
+        dde-calendar
+        dde-clipboard
+        deepin-kwin
+      ];
+    };
+}
+
diff --git a/nixos/modules/services/x11/desktop-managers/default.nix b/nixos/modules/services/x11/desktop-managers/default.nix
index 510561246a2..66cb4ee29c0 100644
--- a/nixos/modules/services/x11/desktop-managers/default.nix
+++ b/nixos/modules/services/x11/desktop-managers/default.nix
@@ -21,7 +21,7 @@ in
     ./none.nix ./xterm.nix ./phosh.nix ./xfce.nix ./plasma5.nix ./lumina.nix
     ./lxqt.nix ./enlightenment.nix ./gnome.nix ./retroarch.nix ./kodi.nix
     ./mate.nix ./pantheon.nix ./surf-display.nix ./cde.nix
-    ./cinnamon.nix
+    ./cinnamon.nix ./budgie.nix ./deepin.nix
   ];
 
   options = {
diff --git a/nixos/modules/services/x11/desktop-managers/gnome.md b/nixos/modules/services/x11/desktop-managers/gnome.md
new file mode 100644
index 00000000000..d9e75bfe6bd
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/gnome.md
@@ -0,0 +1,167 @@
+# GNOME Desktop {#chap-gnome}
+
+GNOME provides a simple, yet full-featured desktop environment with a focus on productivity. Its Mutter compositor supports both Wayland and X server, and the GNOME Shell user interface is fully customizable by extensions.
+
+## Enabling GNOME {#sec-gnome-enable}
+
+All of the core apps, optional apps, games, and core developer tools from GNOME are available.
+
+To enable the GNOME desktop use:
+
+```
+services.xserver.desktopManager.gnome.enable = true;
+services.xserver.displayManager.gdm.enable = true;
+```
+
+::: {.note}
+While it is not strictly necessary to use GDM as the display manager with GNOME, it is recommended, as some features such as screen lock [might not work](#sec-gnome-faq-can-i-use-lightdm-with-gnome) without it.
+:::
+
+The default applications used in NixOS are very minimal, inspired by the defaults used in [gnome-build-meta](https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/40.0/elements/core/meta-gnome-core-utilities.bst).
+
+### GNOME without the apps {#sec-gnome-without-the-apps}
+
+If you’d like to only use the GNOME desktop and not the apps, you can disable them with:
+
+```
+services.gnome.core-utilities.enable = false;
+```
+
+and none of them will be installed.
+
+If you’d only like to omit a subset of the core utilities, you can use
+[](#opt-environment.gnome.excludePackages).
+Note that this mechanism can only exclude core utilities, games and core developer tools.
+
+### Disabling GNOME services {#sec-gnome-disabling-services}
+
+It is also possible to disable many of the [core services](https://github.com/NixOS/nixpkgs/blob/b8ec4fd2a4edc4e30d02ba7b1a2cc1358f3db1d5/nixos/modules/services/x11/desktop-managers/gnome.nix#L329-L348). For example, if you do not need indexing files, you can disable Tracker with:
+
+```
+services.gnome.tracker-miners.enable = false;
+services.gnome.tracker.enable = false;
+```
+
+Note, however, that doing so is not supported and might break some applications. Notably, GNOME Music cannot work without Tracker.
+
+### GNOME games {#sec-gnome-games}
+
+You can install all of the GNOME games with:
+
+```
+services.gnome.games.enable = true;
+```
+
+### GNOME core developer tools {#sec-gnome-core-developer-tools}
+
+You can install GNOME core developer tools with:
+
+```
+services.gnome.core-developer-tools.enable = true;
+```
+
+## Enabling GNOME Flashback {#sec-gnome-enable-flashback}
+
+GNOME Flashback provides a desktop environment based on the classic GNOME 2 architecture. You can enable the default GNOME Flashback session, which uses the Metacity window manager, with:
+
+```
+services.xserver.desktopManager.gnome.flashback.enableMetacity = true;
+```
+
+It is also possible to create custom sessions that replace Metacity with a different window manager using [](#opt-services.xserver.desktopManager.gnome.flashback.customSessions).
+
+The following example uses `xmonad` window manager:
+
+```
+services.xserver.desktopManager.gnome.flashback.customSessions = [
+  {
+    wmName = "xmonad";
+    wmLabel = "XMonad";
+    wmCommand = "${pkgs.haskellPackages.xmonad}/bin/xmonad";
+    enableGnomePanel = false;
+  }
+];
+```
+
+## Icons and GTK Themes {#sec-gnome-icons-and-gtk-themes}
+
+Icon themes and GTK themes don’t require any special option to install in NixOS.
+
+You can add them to [](#opt-environment.systemPackages) and switch to them with GNOME Tweaks.
+If you’d like to do this manually in dconf, change the values of the following keys:
+
+```
+/org/gnome/desktop/interface/gtk-theme
+/org/gnome/desktop/interface/icon-theme
+```
+
+in `dconf-editor`
+
+## Shell Extensions {#sec-gnome-shell-extensions}
+
+Most Shell extensions are packaged under the `gnomeExtensions` attribute.
+Some packages that include Shell extensions, like `gnome.gpaste`, don’t have their extension decoupled under this attribute.
+
+You can install them like any other package:
+
+```
+environment.systemPackages = [
+  gnomeExtensions.dash-to-dock
+  gnomeExtensions.gsconnect
+  gnomeExtensions.mpris-indicator-button
+];
+```
+
+Unfortunately, we lack a way for these to be managed in a completely declarative way.
+So you have to enable them manually with an Extensions application.
+It is possible to use a [GSettings override](#sec-gnome-gsettings-overrides) for this on `org.gnome.shell.enabled-extensions`, but that will only influence the default value.
+
+## GSettings Overrides {#sec-gnome-gsettings-overrides}
+
+Majority of software building on the GNOME platform use GLib’s [GSettings](https://developer.gnome.org/gio/unstable/GSettings.html) system to manage runtime configuration. For our purposes, the system consists of XML schemas describing the individual configuration options, stored in the package, and a settings backend, where the values of the settings are stored. On NixOS, like on most Linux distributions, dconf database is used as the backend.
+
+[GSettings vendor overrides](https://developer.gnome.org/gio/unstable/GSettings.html#id-1.4.19.2.9.25) can be used to adjust the default values for settings of the GNOME desktop and apps by replacing the default values specified in the XML schemas. Using overrides will allow you to pre-seed user settings before you even start the session.
+
+::: {.warning}
+Overrides really only change the default values for GSettings keys so if you or an application changes the setting value, the value set by the override will be ignored. Until [NixOS’s dconf module implements changing values](https://github.com/NixOS/nixpkgs/issues/54150), you will either need to keep that in mind and clear the setting from the backend using `dconf reset` command when that happens, or use the [module from home-manager](https://nix-community.github.io/home-manager/options.html#opt-dconf.settings).
+:::
+
+You can override the default GSettings values using the
+[](#opt-services.xserver.desktopManager.gnome.extraGSettingsOverrides) option.
+
+Take note that whatever packages you want to override GSettings for, you need to add them to
+[](#opt-services.xserver.desktopManager.gnome.extraGSettingsOverridePackages).
+
+You can use `dconf-editor` tool to explore which GSettings you can set.
+
+### Example {#sec-gnome-gsettings-overrides-example}
+
+```
+services.xserver.desktopManager.gnome = {
+  extraGSettingsOverrides = ''
+    # Change default background
+    [org.gnome.desktop.background]
+    picture-uri='file://${pkgs.nixos-artwork.wallpapers.mosaic-blue.gnomeFilePath}'
+
+    # Favorite apps in gnome-shell
+    [org.gnome.shell]
+    favorite-apps=['org.gnome.Photos.desktop', 'org.gnome.Nautilus.desktop']
+  '';
+
+  extraGSettingsOverridePackages = [
+    pkgs.gsettings-desktop-schemas # for org.gnome.desktop
+    pkgs.gnome.gnome-shell # for org.gnome.shell
+  ];
+};
+```
+
+## Frequently Asked Questions {#sec-gnome-faq}
+
+### Can I use LightDM with GNOME? {#sec-gnome-faq-can-i-use-lightdm-with-gnome}
+
+Yes you can, and any other display-manager in NixOS.
+
+However, it doesn’t work correctly for the Wayland session of GNOME Shell yet, and
+won’t be able to lock your screen.
+
+See [this issue.](https://github.com/NixOS/nixpkgs/issues/56342)
diff --git a/nixos/modules/services/x11/desktop-managers/gnome.nix b/nixos/modules/services/x11/desktop-managers/gnome.nix
index 9c1978e362b..79b2e7c6ead 100644
--- a/nixos/modules/services/x11/desktop-managers/gnome.nix
+++ b/nixos/modules/services/x11/desktop-managers/gnome.nix
@@ -66,7 +66,7 @@ in
 {
 
   meta = {
-    doc = ./gnome.xml;
+    doc = ./gnome.md;
     maintainers = teams.gnome.members;
   };
 
@@ -352,8 +352,8 @@ in
         })
       ];
 
-      # Harmonize Qt5 application style and also make them use the portal for file chooser dialog.
-      qt5 = {
+      # Harmonize Qt application style and also make them use the portal for file chooser dialog.
+      qt = {
         enable = mkDefault true;
         platformTheme = mkDefault "gnome";
         style = mkDefault "adwaita";
diff --git a/nixos/modules/services/x11/desktop-managers/gnome.xml b/nixos/modules/services/x11/desktop-managers/gnome.xml
deleted file mode 100644
index 807c9d64e20..00000000000
--- a/nixos/modules/services/x11/desktop-managers/gnome.xml
+++ /dev/null
@@ -1,253 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xml:id="chap-gnome">
- <title>GNOME Desktop</title>
- <para>
-  GNOME provides a simple, yet full-featured desktop environment with a focus on productivity. Its Mutter compositor supports both Wayland and X server, and the GNOME Shell user interface is fully customizable by extensions.
- </para>
-
- <section xml:id="sec-gnome-enable">
-  <title>Enabling GNOME</title>
-
-  <para>
-   All of the core apps, optional apps, games, and core developer tools from GNOME are available.
-  </para>
-
-  <para>
-   To enable the GNOME desktop use:
-  </para>
-
-<programlisting>
-<xref linkend="opt-services.xserver.desktopManager.gnome.enable"/> = true;
-<xref linkend="opt-services.xserver.displayManager.gdm.enable"/> = true;
-</programlisting>
-
-  <note>
-   <para>
-    While it is not strictly necessary to use GDM as the display manager with GNOME, it is recommended, as some features such as screen lock <link xlink:href="#sec-gnome-faq-can-i-use-lightdm-with-gnome">might not work</link> without it.
-   </para>
-  </note>
-
-  <para>
-   The default applications used in NixOS are very minimal, inspired by the defaults used in <link xlink:href="https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/40.0/elements/core/meta-gnome-core-utilities.bst">gnome-build-meta</link>.
-  </para>
-
-  <section xml:id="sec-gnome-without-the-apps">
-   <title>GNOME without the apps</title>
-
-   <para>
-    If you’d like to only use the GNOME desktop and not the apps, you can disable them with:
-   </para>
-
-<programlisting>
-<xref linkend="opt-services.gnome.core-utilities.enable"/> = false;
-</programlisting>
-
-   <para>
-    and none of them will be installed.
-   </para>
-
-   <para>
-    If you’d only like to omit a subset of the core utilities, you can use <xref linkend="opt-environment.gnome.excludePackages"/>.
-    Note that this mechanism can only exclude core utilities, games and core developer tools.
-   </para>
-  </section>
-
-  <section xml:id="sec-gnome-disabling-services">
-   <title>Disabling GNOME services</title>
-
-   <para>
-    It is also possible to disable many of the <link xlink:href="https://github.com/NixOS/nixpkgs/blob/b8ec4fd2a4edc4e30d02ba7b1a2cc1358f3db1d5/nixos/modules/services/x11/desktop-managers/gnome.nix#L329-L348">core services</link>. For example, if you do not need indexing files, you can disable Tracker with:
-   </para>
-
-<programlisting>
-<xref linkend="opt-services.gnome.tracker-miners.enable"/> = false;
-<xref linkend="opt-services.gnome.tracker.enable"/> = false;
-</programlisting>
-
-   <para>
-    Note, however, that doing so is not supported and might break some applications. Notably, GNOME Music cannot work without Tracker.
-   </para>
-  </section>
-
-  <section xml:id="sec-gnome-games">
-   <title>GNOME games</title>
-
-   <para>
-    You can install all of the GNOME games with:
-   </para>
-
-<programlisting>
-<xref linkend="opt-services.gnome.games.enable"/> = true;
-</programlisting>
-  </section>
-
-  <section xml:id="sec-gnome-core-developer-tools">
-   <title>GNOME core developer tools</title>
-
-   <para>
-    You can install GNOME core developer tools with:
-   </para>
-
-<programlisting>
-<xref linkend="opt-services.gnome.core-developer-tools.enable"/> = true;
-</programlisting>
-  </section>
- </section>
-
- <section xml:id="sec-gnome-enable-flashback">
-  <title>Enabling GNOME Flashback</title>
-
-  <para>
-   GNOME Flashback provides a desktop environment based on the classic GNOME 2 architecture. You can enable the default GNOME Flashback session, which uses the Metacity window manager, with:
-  </para>
-
-<programlisting>
-<xref linkend="opt-services.xserver.desktopManager.gnome.flashback.enableMetacity"/> = true;
-</programlisting>
-
-  <para>
-   It is also possible to create custom sessions that replace Metacity with a different window manager using <xref linkend="opt-services.xserver.desktopManager.gnome.flashback.customSessions"/>.
-  </para>
-
-  <para>
-   The following example uses <literal>xmonad</literal> window manager:
-  </para>
-
-<programlisting>
-<xref linkend="opt-services.xserver.desktopManager.gnome.flashback.customSessions"/> = [
-  {
-    wmName = "xmonad";
-    wmLabel = "XMonad";
-    wmCommand = "${pkgs.haskellPackages.xmonad}/bin/xmonad";
-    enableGnomePanel = false;
-  }
-];
-</programlisting>
-
- </section>
-
- <section xml:id="sec-gnome-icons-and-gtk-themes">
-  <title>Icons and GTK Themes</title>
-
-  <para>
-   Icon themes and GTK themes don’t require any special option to install in NixOS.
-  </para>
-
-  <para>
-   You can add them to <xref linkend="opt-environment.systemPackages"/> and switch to them with GNOME Tweaks.
-   If you’d like to do this manually in dconf, change the values of the following keys:
-  </para>
-
-<programlisting>
-/org/gnome/desktop/interface/gtk-theme
-/org/gnome/desktop/interface/icon-theme
-</programlisting>
-
-  <para>
-   in <literal>dconf-editor</literal>
-  </para>
- </section>
-
- <section xml:id="sec-gnome-shell-extensions">
-  <title>Shell Extensions</title>
-
-  <para>
-   Most Shell extensions are packaged under the <literal>gnomeExtensions</literal> attribute.
-   Some packages that include Shell extensions, like <literal>gnome.gpaste</literal>, don’t have their extension decoupled under this attribute.
-  </para>
-
-  <para>
-   You can install them like any other package:
-  </para>
-
-<programlisting>
-<xref linkend="opt-environment.systemPackages"/> = [
-  gnomeExtensions.dash-to-dock
-  gnomeExtensions.gsconnect
-  gnomeExtensions.mpris-indicator-button
-];
-</programlisting>
-
-  <para>
-   Unfortunately, we lack a way for these to be managed in a completely declarative way.
-   So you have to enable them manually with an Extensions application.
-   It is possible to use a <link xlink:href="#sec-gnome-gsettings-overrides">GSettings override</link> for this on <literal>org.gnome.shell.enabled-extensions</literal>, but that will only influence the default value.
-  </para>
- </section>
-
- <section xml:id="sec-gnome-gsettings-overrides">
-  <title>GSettings Overrides</title>
-
-  <para>
-   Majority of software building on the GNOME platform use GLib’s <link xlink:href="https://developer.gnome.org/gio/unstable/GSettings.html">GSettings</link> system to manage runtime configuration. For our purposes, the system consists of XML schemas describing the individual configuration options, stored in the package, and a settings backend, where the values of the settings are stored. On NixOS, like on most Linux distributions, dconf database is used as the backend.
-  </para>
-
-  <para>
-   <link xlink:href="https://developer.gnome.org/gio/unstable/GSettings.html#id-1.4.19.2.9.25">GSettings vendor overrides</link> can be used to adjust the default values for settings of the GNOME desktop and apps by replacing the default values specified in the XML schemas. Using overrides will allow you to pre-seed user settings before you even start the session.
-  </para>
-
-  <warning>
-   <para>
-    Overrides really only change the default values for GSettings keys so if you or an application changes the setting value, the value set by the override will be ignored. Until <link xlink:href="https://github.com/NixOS/nixpkgs/issues/54150">NixOS’s dconf module implements changing values</link>, you will either need to keep that in mind and clear the setting from the backend using <literal>dconf reset</literal> command when that happens, or use the <link xlink:href="https://nix-community.github.io/home-manager/options.html#opt-dconf.settings">module from home-manager</link>.
-   </para>
-  </warning>
-
-  <para>
-   You can override the default GSettings values using the <xref linkend="opt-services.xserver.desktopManager.gnome.extraGSettingsOverrides"/> option.
-  </para>
-
-  <para>
-   Take note that whatever packages you want to override GSettings for, you need to add them to
-   <xref linkend="opt-services.xserver.desktopManager.gnome.extraGSettingsOverridePackages"/>.
-  </para>
-
-  <para>
-   You can use <literal>dconf-editor</literal> tool to explore which GSettings you can set.
-  </para>
-
-  <section xml:id="sec-gnome-gsettings-overrides-example">
-   <title>Example</title>
-
-<programlisting>
-services.xserver.desktopManager.gnome = {
-  <link xlink:href="#opt-services.xserver.desktopManager.gnome.extraGSettingsOverrides">extraGSettingsOverrides</link> = ''
-    # Change default background
-    [org.gnome.desktop.background]
-    picture-uri='file://${pkgs.nixos-artwork.wallpapers.mosaic-blue.gnomeFilePath}'
-
-    # Favorite apps in gnome-shell
-    [org.gnome.shell]
-    favorite-apps=['org.gnome.Photos.desktop', 'org.gnome.Nautilus.desktop']
-  '';
-
-  <link xlink:href="#opt-services.xserver.desktopManager.gnome.extraGSettingsOverridePackages">extraGSettingsOverridePackages</link> = [
-    pkgs.gsettings-desktop-schemas # for org.gnome.desktop
-    pkgs.gnome.gnome-shell # for org.gnome.shell
-  ];
-};
-</programlisting>
-  </section>
- </section>
-
- <section xml:id="sec-gnome-faq">
-  <title>Frequently Asked Questions</title>
-
-  <section xml:id="sec-gnome-faq-can-i-use-lightdm-with-gnome">
-   <title>Can I use LightDM with GNOME?</title>
-
-   <para>
-    Yes you can, and any other display-manager in NixOS.
-   </para>
-
-   <para>
-    However, it doesn’t work correctly for the Wayland session of GNOME Shell yet, and
-    won’t be able to lock your screen.
-   </para>
-
-   <para>
-    See <link xlink:href="https://github.com/NixOS/nixpkgs/issues/56342">this issue.</link>
-   </para>
-  </section>
- </section>
-</chapter>
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.md b/nixos/modules/services/x11/desktop-managers/pantheon.md
new file mode 100644
index 00000000000..1c14ede8474
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.md
@@ -0,0 +1,74 @@
+# Pantheon Desktop {#chap-pantheon}
+
+Pantheon is the desktop environment created for the elementary OS distribution. It is written from scratch in Vala, utilizing GNOME technologies with GTK and Granite.
+
+## Enabling Pantheon {#sec-pantheon-enable}
+
+All of Pantheon is working in NixOS and the applications should be available, aside from a few [exceptions](https://github.com/NixOS/nixpkgs/issues/58161). To enable Pantheon, set
+```
+services.xserver.desktopManager.pantheon.enable = true;
+```
+This automatically enables LightDM and Pantheon's LightDM greeter. If you'd like to disable this, set
+```
+services.xserver.displayManager.lightdm.greeters.pantheon.enable = false;
+services.xserver.displayManager.lightdm.enable = false;
+```
+but please be aware using Pantheon without LightDM as a display manager will break screenlocking from the UI. The NixOS module for Pantheon installs all of Pantheon's default applications. If you'd like to not install Pantheon's apps, set
+```
+services.pantheon.apps.enable = false;
+```
+You can also use [](#opt-environment.pantheon.excludePackages) to remove any other app (like `elementary-mail`).
+
+## Wingpanel and Switchboard plugins {#sec-pantheon-wingpanel-switchboard}
+
+Wingpanel and Switchboard work differently than they do in other distributions, as far as using plugins. You cannot install a plugin globally (like with {option}`environment.systemPackages`) to start using it. You should instead be using the following options:
+
+  - [](#opt-services.xserver.desktopManager.pantheon.extraWingpanelIndicators)
+  - [](#opt-services.xserver.desktopManager.pantheon.extraSwitchboardPlugs)
+
+to configure the programs with plugs or indicators.
+
+The difference in NixOS is both these programs are patched to load plugins from a directory that is the value of an environment variable. All of which is controlled in Nix. If you need to configure the particular packages manually you can override the packages like:
+```
+wingpanel-with-indicators.override {
+  indicators = [
+    pkgs.some-special-indicator
+  ];
+};
+
+switchboard-with-plugs.override {
+  plugs = [
+    pkgs.some-special-plug
+  ];
+};
+```
+please note that, like how the NixOS options describe these as extra plugins, this would only add to the default plugins included with the programs. If for some reason you'd like to configure which plugins to use exactly, both packages have an argument for this:
+```
+wingpanel-with-indicators.override {
+  useDefaultIndicators = false;
+  indicators = specialListOfIndicators;
+};
+
+switchboard-with-plugs.override {
+  useDefaultPlugs = false;
+  plugs = specialListOfPlugs;
+};
+```
+this could be most useful for testing a particular plug-in in isolation.
+
+## FAQ {#sec-pantheon-faq}
+
+[I have switched from a different desktop and Pantheon’s theming looks messed up.]{#sec-pantheon-faq-messed-up-theme}
+  : Open Switchboard and go to: Administration → About → Restore Default Settings → Restore Settings. This will reset any dconf settings to their Pantheon defaults. Note this could reset certain GNOME specific preferences if that desktop was used prior.
+
+[I cannot enable both GNOME and Pantheon.]{#sec-pantheon-faq-gnome-and-pantheon}
+  : This is a known [issue](https://github.com/NixOS/nixpkgs/issues/64611) and there is no known workaround.
+
+[Does AppCenter work, or is it available?]{#sec-pantheon-faq-appcenter}
+  : AppCenter has been available since 20.03. Starting from 21.11, the Flatpak backend should work so you can install some Flatpak applications using it. However, due to missing appstream metadata, the Packagekit backend does not function currently. See this [issue](https://github.com/NixOS/nixpkgs/issues/15932).
+
+    If you are using Pantheon, AppCenter should be installed by default if you have [Flatpak support](#module-services-flatpak) enabled. If you also wish to add the `appcenter` Flatpak remote:
+
+    ```ShellSession
+    $ flatpak remote-add --if-not-exists appcenter https://flatpak.elementary.io/repo.flatpakrepo
+    ```
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
index 5c0203224e1..e87ae5ae812 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -17,7 +17,7 @@ in
 {
 
   meta = {
-    doc = ./pantheon.xml;
+    doc = ./pantheon.md;
     maintainers = teams.pantheon.members;
   };
 
@@ -113,6 +113,7 @@ in
 
       services.xserver.displayManager.sessionCommands = ''
         if test "$XDG_CURRENT_DESKTOP" = "Pantheon"; then
+            true
             ${concatMapStrings (p: ''
               if [ -d "${p}/share/gsettings-schemas/${p.name}" ]; then
                 export XDG_DATA_DIRS=$XDG_DATA_DIRS''${XDG_DATA_DIRS:+:}${p}/share/gsettings-schemas/${p.name}
@@ -169,6 +170,9 @@ in
       };
       services.udev.packages = [
         pkgs.pantheon.gnome-settings-daemon
+        # Force enable KMS modifiers for devices that require them.
+        # https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1443
+        pkgs.pantheon.mutter
       ];
       systemd.packages = [
         pkgs.pantheon.gnome-settings-daemon
@@ -224,11 +228,16 @@ in
       xdg.icons.enable = true;
 
       xdg.portal.enable = true;
-      xdg.portal.extraPortals = with pkgs.pantheon; [
+      xdg.portal.extraPortals = [
+        # Some Pantheon apps enforce portal usage, we need this for e.g. notifications.
+        # Currently we have buildPortalsInGnome enabled, if you run into issues related
+        # to https://github.com/flatpak/xdg-desktop-portal/issues/656 please report to us.
+        pkgs.xdg-desktop-portal-gtk
+      ] ++ (with pkgs.pantheon; [
         elementary-files
         elementary-settings-daemon
         xdg-desktop-portal-pantheon
-      ];
+      ]);
 
       # Override GSettings schemas
       environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-desktop-schemas}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
@@ -250,10 +259,10 @@ in
       programs.bash.vteIntegration = mkDefault true;
       programs.zsh.vteIntegration = mkDefault true;
 
-      # Harmonize Qt5 applications under Pantheon
-      qt5.enable = true;
-      qt5.platformTheme = "gnome";
-      qt5.style = "adwaita";
+      # Harmonize Qt applications under Pantheon
+      qt.enable = true;
+      qt.platformTheme = "gnome";
+      qt.style = "adwaita";
 
       # Default Fonts
       fonts.fonts = with pkgs; [
@@ -285,7 +294,7 @@ in
         elementary-music
         elementary-photos
         elementary-screenshot
-        # elementary-tasks
+        elementary-tasks
         elementary-terminal
         elementary-videos
         epiphany
@@ -306,7 +315,6 @@ in
       environment.systemPackages = with pkgs.pantheon; [
         contractor
         file-roller-contract
-        gnome-bluetooth-contract
       ];
 
       environment.pathsToLink = [
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.xml b/nixos/modules/services/x11/desktop-managers/pantheon.xml
deleted file mode 100644
index 6226f8f6a27..00000000000
--- a/nixos/modules/services/x11/desktop-managers/pantheon.xml
+++ /dev/null
@@ -1,120 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xml:id="chap-pantheon">
- <title>Pantheon Desktop</title>
- <para>
-  Pantheon is the desktop environment created for the elementary OS distribution. It is written from scratch in Vala, utilizing GNOME technologies with GTK and Granite.
- </para>
- <section xml:id="sec-pantheon-enable">
-  <title>Enabling Pantheon</title>
-
-  <para>
-   All of Pantheon is working in NixOS and the applications should be available, aside from a few <link xlink:href="https://github.com/NixOS/nixpkgs/issues/58161">exceptions</link>. To enable Pantheon, set
-<programlisting>
-<xref linkend="opt-services.xserver.desktopManager.pantheon.enable"/> = true;
-</programlisting>
-   This automatically enables LightDM and Pantheon's LightDM greeter. If you'd like to disable this, set
-<programlisting>
-<xref linkend="opt-services.xserver.displayManager.lightdm.greeters.pantheon.enable"/> = false;
-<xref linkend="opt-services.xserver.displayManager.lightdm.enable"/> = false;
-</programlisting>
-   but please be aware using Pantheon without LightDM as a display manager will break screenlocking from the UI. The NixOS module for Pantheon installs all of Pantheon's default applications. If you'd like to not install Pantheon's apps, set
-<programlisting>
-<xref linkend="opt-services.pantheon.apps.enable"/> = false;
-</programlisting>
-   You can also use <xref linkend="opt-environment.pantheon.excludePackages"/> to remove any other app (like <package>elementary-mail</package>).
-  </para>
- </section>
- <section xml:id="sec-pantheon-wingpanel-switchboard">
-  <title>Wingpanel and Switchboard plugins</title>
-
-  <para>
-   Wingpanel and Switchboard work differently than they do in other distributions, as far as using plugins. You cannot install a plugin globally (like with <option>environment.systemPackages</option>) to start using it. You should instead be using the following options:
-   <itemizedlist>
-    <listitem>
-     <para>
-      <xref linkend="opt-services.xserver.desktopManager.pantheon.extraWingpanelIndicators"/>
-     </para>
-    </listitem>
-    <listitem>
-     <para>
-      <xref linkend="opt-services.xserver.desktopManager.pantheon.extraSwitchboardPlugs"/>
-     </para>
-    </listitem>
-   </itemizedlist>
-   to configure the programs with plugs or indicators.
-  </para>
-
-  <para>
-   The difference in NixOS is both these programs are patched to load plugins from a directory that is the value of an environment variable. All of which is controlled in Nix. If you need to configure the particular packages manually you can override the packages like:
-<programlisting>
-wingpanel-with-indicators.override {
-  indicators = [
-    pkgs.some-special-indicator
-  ];
-};
-
-switchboard-with-plugs.override {
-  plugs = [
-    pkgs.some-special-plug
-  ];
-};
-</programlisting>
-   please note that, like how the NixOS options describe these as extra plugins, this would only add to the default plugins included with the programs. If for some reason you'd like to configure which plugins to use exactly, both packages have an argument for this:
-<programlisting>
-wingpanel-with-indicators.override {
-  useDefaultIndicators = false;
-  indicators = specialListOfIndicators;
-};
-
-switchboard-with-plugs.override {
-  useDefaultPlugs = false;
-  plugs = specialListOfPlugs;
-};
-</programlisting>
-   this could be most useful for testing a particular plug-in in isolation.
-  </para>
- </section>
- <section xml:id="sec-pantheon-faq">
-  <title>FAQ</title>
-
-  <variablelist>
-   <varlistentry xml:id="sec-pantheon-faq-messed-up-theme">
-    <term>
-     I have switched from a different desktop and Pantheon’s theming looks messed up.
-    </term>
-    <listitem>
-     <para>
-      Open Switchboard and go to: <guilabel>Administration</guilabel> → <guilabel>About</guilabel> → <guilabel>Restore Default Settings</guilabel> → <guibutton>Restore Settings</guibutton>. This will reset any dconf settings to their Pantheon defaults. Note this could reset certain GNOME specific preferences if that desktop was used prior.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry xml:id="sec-pantheon-faq-gnome-and-pantheon">
-    <term>
-     I cannot enable both GNOME and Pantheon.
-    </term>
-    <listitem>
-     <para>
-      This is a known <link xlink:href="https://github.com/NixOS/nixpkgs/issues/64611">issue</link> and there is no known workaround.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry xml:id="sec-pantheon-faq-appcenter">
-    <term>
-     Does AppCenter work, or is it available?
-    </term>
-    <listitem>
-     <para>
-      AppCenter has been available since 20.03. Starting from 21.11, the Flatpak backend should work so you can install some Flatpak applications using it. However, due to missing appstream metadata, the Packagekit backend does not function currently. See this <link xlink:href="https://github.com/NixOS/nixpkgs/issues/15932">issue</link>.
-     </para>
-     <para>
-      If you are using Pantheon, AppCenter should be installed by default if you have <link linkend="module-services-flatpak">Flatpak support</link> enabled. If you also wish to add the <literal>appcenter</literal> Flatpak remote:
-     </para>
-<screen>
-<prompt>$ </prompt>flatpak remote-add --if-not-exists appcenter https://flatpak.elementary.io/repo.flatpakrepo
-</screen>
-    </listitem>
-   </varlistentry>
-  </variablelist>
- </section>
-</chapter>
diff --git a/nixos/modules/services/x11/desktop-managers/phosh.nix b/nixos/modules/services/x11/desktop-managers/phosh.nix
index e889c0e34e7..3cfa6e044b7 100644
--- a/nixos/modules/services/x11/desktop-managers/phosh.nix
+++ b/nixos/modules/services/x11/desktop-managers/phosh.nix
@@ -173,7 +173,7 @@ in
     systemd.services.phosh = {
       wantedBy = [ "graphical.target" ];
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/phosh";
+        ExecStart = "${cfg.package}/bin/phosh-session";
         User = cfg.user;
         Group = cfg.group;
         PAMName = "login";
diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix
index eb30e601dd0..38f932ffb42 100644
--- a/nixos/modules/services/x11/desktop-managers/plasma5.nix
+++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix
@@ -28,51 +28,10 @@ let
 
   libsForQt5 = pkgs.plasma5Packages;
   inherit (libsForQt5) kdeGear kdeFrameworks plasma5;
-  inherit (pkgs) writeText;
   inherit (lib)
     getBin optionalString literalExpression
     mkRemovedOptionModule mkRenamedOptionModule
-    mkDefault mkIf mkMerge mkOption mkPackageOption types;
-
-  ini = pkgs.formats.ini { };
-
-  gtkrc2 = writeText "gtkrc-2.0" ''
-    # Default GTK+ 2 config for NixOS Plasma 5
-    include "/run/current-system/sw/share/themes/Breeze/gtk-2.0/gtkrc"
-    style "user-font"
-    {
-      font_name="Sans Serif Regular"
-    }
-    widget_class "*" style "user-font"
-    gtk-font-name="Sans Serif Regular 10"
-    gtk-theme-name="Breeze"
-    gtk-icon-theme-name="breeze"
-    gtk-fallback-icon-theme="hicolor"
-    gtk-cursor-theme-name="breeze_cursors"
-    gtk-toolbar-style=GTK_TOOLBAR_ICONS
-    gtk-menu-images=1
-    gtk-button-images=1
-  '';
-
-  gtk3_settings = ini.generate "settings.ini" {
-    Settings = {
-      gtk-font-name = "Sans Serif Regular 10";
-      gtk-theme-name = "Breeze";
-      gtk-icon-theme-name = "breeze";
-      gtk-fallback-icon-theme = "hicolor";
-      gtk-cursor-theme-name = "breeze_cursors";
-      gtk-toolbar-style = "GTK_TOOLBAR_ICONS";
-      gtk-menu-images = 1;
-      gtk-button-images = 1;
-    };
-  };
-
-  kcminputrc = ini.generate "kcminputrc" {
-    Mouse = {
-      cursorTheme = "breeze_cursors";
-      cursorSize = 0;
-    };
-  };
+    mkDefault mkIf mkMerge mkOption mkPackageOptionMD types;
 
   activationScript = ''
     ${set_XDG_CONFIG_HOME}
@@ -119,133 +78,93 @@ let
     XDG_CONFIG_HOME=''${XDG_CONFIG_HOME:-$HOME/.config}
   '';
 
-  startplasma = ''
-    ${set_XDG_CONFIG_HOME}
-    mkdir -p "''${XDG_CONFIG_HOME}"
-  '' + optionalString config.hardware.pulseaudio.enable ''
-    # Load PulseAudio module for routing support.
-    # See also: http://colin.guthr.ie/2009/10/so-how-does-the-kde-pulseaudio-support-work-anyway/
-      ${getBin config.hardware.pulseaudio.package}/bin/pactl load-module module-device-manager "do_routing=1"
-  '' + ''
-    ${activationScript}
-
-    # Create default configurations if Plasma has never been started.
-    kdeglobals="''${XDG_CONFIG_HOME}/kdeglobals"
-    if ! [ -f "$kdeglobals" ]; then
-      kcminputrc="''${XDG_CONFIG_HOME}/kcminputrc"
-      if ! [ -f "$kcminputrc" ]; then
-          cat ${kcminputrc} >"$kcminputrc"
-      fi
-
-      gtkrc2="$HOME/.gtkrc-2.0"
-      if ! [ -f "$gtkrc2" ]; then
-          cat ${gtkrc2} >"$gtkrc2"
-      fi
-
-      gtk3_settings="''${XDG_CONFIG_HOME}/gtk-3.0/settings.ini"
-      if ! [ -f "$gtk3_settings" ]; then
-          mkdir -p "$(dirname "$gtk3_settings")"
-          cat ${gtk3_settings} >"$gtk3_settings"
-      fi
-    fi
-  '';
-
 in
 
 {
-  options.services.xserver.desktopManager.plasma5 = {
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc "Enable the Plasma 5 (KDE 5) desktop environment.";
-    };
-
-    phononBackend = mkOption {
-      type = types.enum [ "gstreamer" "vlc" ];
-      default = "gstreamer";
-      example = "vlc";
-      description = lib.mdDoc "Phonon audio backend to install.";
-    };
-
-    supportDDC = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Support setting monitor brightness via DDC.
-
-        This is not needed for controlling brightness of the internal monitor
-        of a laptop and as it is considered experimental by upstream, it is
-        disabled by default.
-      '';
-    };
+  options = {
+    services.xserver.desktopManager.plasma5 = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc "Enable the Plasma 5 (KDE 5) desktop environment.";
+      };
 
-    useQtScaling = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc "Enable HiDPI scaling in Qt.";
-    };
+      phononBackend = mkOption {
+        type = types.enum [ "gstreamer" "vlc" ];
+        default = "vlc";
+        example = "gstreamer";
+        description = lib.mdDoc "Phonon audio backend to install.";
+      };
 
-    runUsingSystemd = mkOption {
-      description = lib.mdDoc "Use systemd to manage the Plasma session";
-      type = types.bool;
-      default = true;
-    };
+      useQtScaling = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc "Enable HiDPI scaling in Qt.";
+      };
 
-    excludePackages = mkOption {
-      description = lib.mdDoc "List of default packages to exclude from the configuration";
-      type = types.listOf types.package;
-      default = [];
-      example = literalExpression "[ pkgs.plasma5Packages.oxygen ]";
-    };
+      runUsingSystemd = mkOption {
+        description = lib.mdDoc "Use systemd to manage the Plasma session";
+        type = types.bool;
+        default = true;
+      };
 
-    notoPackage = mkPackageOption pkgs "Noto fonts" {
-      default = [ "noto-fonts" ];
-      example = "noto-fonts-lgc-plus";
-    };
+      notoPackage = mkPackageOptionMD pkgs "Noto fonts" {
+        default = [ "noto-fonts" ];
+        example = "noto-fonts-lgc-plus";
+      };
 
-    # Internally allows configuring kdeglobals globally
-    kdeglobals = mkOption {
-      internal = true;
-      default = {};
-      type = kdeConfigurationType;
-    };
+      # Internally allows configuring kdeglobals globally
+      kdeglobals = mkOption {
+        internal = true;
+        default = {};
+        type = kdeConfigurationType;
+      };
 
-    # Internally allows configuring kwin globally
-    kwinrc = mkOption {
-      internal = true;
-      default = {};
-      type = kdeConfigurationType;
-    };
+      # Internally allows configuring kwin globally
+      kwinrc = mkOption {
+        internal = true;
+        default = {};
+        type = kdeConfigurationType;
+      };
 
-    mobile.enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Enable support for running the Plasma Mobile shell.
-      '';
-    };
+      mobile.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Enable support for running the Plasma Mobile shell.
+        '';
+      };
 
-    mobile.installRecommendedSoftware = mkOption {
-      type = types.bool;
-      default = true;
-      description = lib.mdDoc ''
-        Installs software recommended for use with Plasma Mobile, but which
-        is not strictly required for Plasma Mobile to run.
-      '';
-    };
+      mobile.installRecommendedSoftware = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc ''
+          Installs software recommended for use with Plasma Mobile, but which
+          is not strictly required for Plasma Mobile to run.
+        '';
+      };
 
-    bigscreen.enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Enable support for running the Plasma Bigscreen session.
-      '';
+      bigscreen.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Enable support for running the Plasma Bigscreen session.
+        '';
+      };
     };
+    environment.plasma5.excludePackages = mkOption {
+        description = lib.mdDoc "List of default packages to exclude from the configuration";
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.plasma5Packages.oxygen ]";
+      };
   };
 
   imports = [
     (mkRemovedOptionModule [ "services" "xserver" "desktopManager" "plasma5" "enableQt4Support" ] "Phonon no longer supports Qt 4.")
+    (mkRemovedOptionModule [ "services" "xserver" "desktopManager" "plasma5" "supportDDC" ] "DDC/CI is no longer supported upstream.")
     (mkRenamedOptionModule [ "services" "xserver" "desktopManager" "kde5" ] [ "services" "xserver" "desktopManager" "plasma5" ])
+    (mkRenamedOptionModule [ "services" "xserver" "desktopManager" "plasma5" "excludePackages" ] [ "environment" "plasma5" "excludePackages" ])
   ];
 
   config = mkMerge [
@@ -273,12 +192,6 @@ in
         };
       };
 
-      # DDC support
-      boot.kernelModules = lib.optional cfg.supportDDC "i2c_dev";
-      services.udev.extraRules = lib.optionalString cfg.supportDDC ''
-        KERNEL=="i2c-[0-9]*", TAG+="uaccess"
-      '';
-
       environment.systemPackages =
         with libsForQt5;
         with plasma5; with kdeGear; with kdeFrameworks;
@@ -365,6 +278,7 @@ in
             pkgs.xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
           ];
           optionalPackages = [
+            pkgs.aha # needed by kinfocenter for fwupd support
             plasma-browser-integration
             konsole
             oxygen
@@ -372,7 +286,7 @@ in
           ];
         in
         requiredPackages
-        ++ utils.removePackagesByName optionalPackages cfg.excludePackages
+        ++ utils.removePackagesByName optionalPackages config.environment.plasma5.excludePackages
 
         # Phonon audio backend
         ++ lib.optional (cfg.phononBackend == "gstreamer") libsForQt5.phonon-backend-gstreamer
@@ -387,7 +301,8 @@ in
         ++ lib.optional config.services.colord.enable pkgs.colord-kde
         ++ lib.optional config.services.hardware.bolt.enable pkgs.plasma5Packages.plasma-thunderbolt
         ++ lib.optionals config.services.samba.enable [ kdenetwork-filesharing pkgs.samba ]
-        ++ lib.optional config.services.xserver.wacom.enable pkgs.wacomtablet;
+        ++ lib.optional config.services.xserver.wacom.enable pkgs.wacomtablet
+        ++ lib.optional config.services.flatpak.enable flatpak-kcm;
 
       # Extra services for D-Bus activation
       services.dbus.packages = [
@@ -401,7 +316,18 @@ in
 
       environment.etc."X11/xkb".source = xcfg.xkbDir;
 
-      environment.sessionVariables.PLASMA_USE_QT_SCALING = mkIf cfg.useQtScaling "1";
+      environment.sessionVariables = {
+        PLASMA_USE_QT_SCALING = mkIf cfg.useQtScaling "1";
+
+        # Needed for things that depend on other store.kde.org packages to install correctly,
+        # notably Plasma look-and-feel packages (a.k.a. Global Themes)
+        #
+        # FIXME: this is annoyingly impure and should really be fixed at source level somehow,
+        # but kpackage is a library so we can't just wrap the one thing invoking it and be done.
+        # This also means things won't work for people not on Plasma, but at least this way it
+        # works for SOME people.
+        KPACKAGE_DEP_RESOLVERS_PATH = "${pkgs.plasma5Packages.frameworkintegration.out}/libexec/kf5/kpackagehandlers";
+      };
 
       # Enable GTK applications to load SVG icons
       services.xserver.gdk-pixbuf.modulePackages = [ pkgs.librsvg ];
@@ -438,12 +364,7 @@ in
 
       security.pam.services.kde = { allowNullPassword = true; };
 
-      # Doing these one by one seems silly, but we currently lack a better
-      # construct for handling common pam configs.
-      security.pam.services.gdm.enableKwallet = true;
-      security.pam.services.kdm.enableKwallet = true;
-      security.pam.services.lightdm.enableKwallet = true;
-      security.pam.services.sddm.enableKwallet = true;
+      security.pam.services.login.enableKwallet = true;
 
       systemd.user.services = {
         plasma-early-setup = mkIf cfg.runUsingSystemd {
@@ -462,7 +383,6 @@ in
 
       # Update the start menu for each user that is currently logged in
       system.userActivationScripts.plasmaSetup = activationScript;
-      services.xserver.displayManager.setupCommands = startplasma;
 
       nixpkgs.config.firefox.enablePlasmaBrowserIntegration = true;
     })
@@ -509,16 +429,19 @@ in
             dolphin-plugins
             ffmpegthumbs
             kdegraphics-thumbnailers
+            kde-inotify-survey
+            kio-admin
             kio-extras
           ];
           optionalPackages = [
+            ark
             elisa
             gwenview
             okular
             khelpcenter
             print-manager
           ];
-      in requiredPackages ++ utils.removePackagesByName optionalPackages cfg.excludePackages;
+      in requiredPackages ++ utils.removePackagesByName optionalPackages config.environment.plasma5.excludePackages;
 
       systemd.user.services = {
         plasma-run-with-systemd = {
diff --git a/nixos/modules/services/x11/display-managers/account-service-util.nix b/nixos/modules/services/x11/display-managers/account-service-util.nix
index 861976d1186..00ffd91cb2f 100644
--- a/nixos/modules/services/x11/display-managers/account-service-util.nix
+++ b/nixos/modules/services/x11/display-managers/account-service-util.nix
@@ -2,7 +2,7 @@
 , glib
 , gobject-introspection
 , python3
-, wrapGAppsHook
+, wrapGAppsNoGuiHook
 , lib
 }:
 
@@ -18,7 +18,7 @@ python3.pkgs.buildPythonApplication {
   strictDeps = false;
 
   nativeBuildInputs = [
-    wrapGAppsHook
+    wrapGAppsNoGuiHook
     gobject-introspection
   ];
 
diff --git a/nixos/modules/services/x11/display-managers/default.nix b/nixos/modules/services/x11/display-managers/default.nix
index 995ecd231c4..1f08ded7c96 100644
--- a/nixos/modules/services/x11/display-managers/default.nix
+++ b/nixos/modules/services/x11/display-managers/default.nix
@@ -27,6 +27,30 @@ let
     Xft.hintstyle: ${fontconfig.hinting.style}
   '';
 
+  # FIXME: this is an ugly hack.
+  # Some sessions (read: most WMs) don't activate systemd's `graphical-session.target`.
+  # Other sessions (read: most non-WMs) expect `graphical-session.target` to be reached
+  # when the entire session is actually ready. We used to just unconditionally force
+  # `graphical-session.target` to be activated in the session wrapper so things like
+  # xdg-autostart-generator work on sessions that are wrong, but this broke sessions
+  # that do things right. So, preserve this behavior (with some extra steps) by matching
+  # on XDG_CURRENT_DESKTOP and deliberately ignoring sessions we know can do the right thing.
+  fakeSession = action: ''
+      session_is_systemd_aware=$(
+        IFS=:
+        for i in $XDG_CURRENT_DESKTOP; do
+          case $i in
+            KDE|GNOME|X-NIXOS-SYSTEMD-AWARE) echo "1"; exit; ;;
+            *) ;;
+          esac
+        done
+      )
+
+      if [ -z "$session_is_systemd_aware" ]; then
+        /run/current-system/systemd/bin/systemctl --user ${action} nixos-fake-graphical-session.target
+      fi
+  '';
+
   # file provided by services.xserver.displayManager.sessionData.wrapper
   xsessionWrapper = pkgs.writeScript "xsession-wrapper"
     ''
@@ -90,8 +114,7 @@ let
 
       ${cfg.displayManager.sessionCommands}
 
-      # Start systemd user services for graphical sessions
-      /run/current-system/systemd/bin/systemctl --user start graphical-session.target
+      ${fakeSession "start"}
 
       # Allow the user to setup a custom session type.
       if test -x ~/.xsession; then
@@ -417,10 +440,10 @@ in
       "XDG_SESSION_ID"
     ];
 
-    systemd.user.targets.graphical-session = {
+    systemd.user.targets.nixos-fake-graphical-session = {
       unitConfig = {
-        RefuseManualStart = false;
-        StopWhenUnneeded = false;
+        Description = "Fake graphical-session target for non-systemd-aware sessions";
+        BindsTo = "graphical-session.target";
       };
     };
 
@@ -451,7 +474,7 @@ in
 
           test -n "$waitPID" && wait "$waitPID"
 
-          /run/current-system/systemd/bin/systemctl --user stop graphical-session.target
+          ${fakeSession "stop"}
 
           exit 0
         '';
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index 1c3881bef2d..676d08b93e2 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -207,7 +207,9 @@ in
     # conflicts display-manager.service, then when nixos-rebuild
     # switch starts multi-user.target, display-manager.service is
     # stopped so plymouth-quit.service can be started.)
-    systemd.services.plymouth-quit.wantedBy = lib.mkForce [];
+    systemd.services.plymouth-quit = mkIf config.boot.plymouth.enable {
+      wantedBy = lib.mkForce [];
+    };
 
     systemd.services.display-manager.serviceConfig = {
       # Restart = "always"; - already defined in xserver.nix
@@ -323,7 +325,7 @@ in
 
         account   sufficient    pam_unix.so
 
-        password  requisite     pam_unix.so nullok sha512
+        password  requisite     pam_unix.so nullok yescrypt
 
         session   optional      pam_keyinit.so revoke
         session   include       login
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
index f74e8efb8f6..548d3c5bc46 100644
--- a/nixos/modules/services/x11/display-managers/lightdm.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -32,7 +32,7 @@ let
   usersConf = writeText "users.conf"
     ''
       [UserList]
-      minimum-uid=500
+      minimum-uid=1000
       hidden-users=${concatStringsSep " " dmcfg.hiddenUsers}
       hidden-shells=/run/current-system/sw/bin/nologin
     '';
@@ -302,7 +302,7 @@ in
 
         account   sufficient    pam_unix.so
 
-        password  requisite     pam_unix.so nullok sha512
+        password  requisite     pam_unix.so nullok yescrypt
 
         session   optional      pam_keyinit.so revoke
         session   include       login
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
index a3f03d7a19a..c04edd0d4b7 100644
--- a/nixos/modules/services/x11/display-managers/sddm.nix
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -215,10 +215,12 @@ in
     };
 
     security.pam.services = {
-      sddm = {
-        allowNullPassword = true;
-        startSession = true;
-      };
+      sddm.text = ''
+        auth      substack      login
+        account   include       login
+        password  substack      login
+        session   include       login
+      '';
 
       sddm-greeter.text = ''
         auth     required       pam_succeed_if.so audit quiet_success user = sddm
@@ -266,6 +268,17 @@ in
     environment.systemPackages = [ sddm ];
     services.dbus.packages = [ sddm ];
 
+    # We're not using the upstream unit, so copy these: https://github.com/sddm/sddm/blob/develop/services/sddm.service.in
+    systemd.services.display-manager.after = [
+      "systemd-user-sessions.service"
+      "getty@tty7.service"
+      "plymouth-quit.service"
+      "systemd-logind.service"
+    ];
+    systemd.services.display-manager.conflicts = [
+      "getty@tty7.service"
+    ];
+
     # To enable user switching, allow sddm to allocate TTYs/displays dynamically.
     services.xserver.tty = null;
     services.xserver.display = null;
diff --git a/nixos/modules/services/x11/extra-layouts.nix b/nixos/modules/services/x11/extra-layouts.nix
index 574657a50c8..1f48713a68d 100644
--- a/nixos/modules/services/x11/extra-layouts.nix
+++ b/nixos/modules/services/x11/extra-layouts.nix
@@ -106,9 +106,9 @@ in
       description = lib.mdDoc ''
         Extra custom layouts that will be included in the xkb configuration.
         Information on how to create a new layout can be found here:
-        [](https://www.x.org/releases/current/doc/xorg-docs/input/XKB-Enhancing.html#Defining_New_Layouts).
+        <https://www.x.org/releases/current/doc/xorg-docs/input/XKB-Enhancing.html#Defining_New_Layouts>.
         For more examples see
-        [](https://wiki.archlinux.org/index.php/X_KeyBoard_extension#Basic_examples)
+        <https://wiki.archlinux.org/index.php/X_KeyBoard_extension#Basic_examples>
       '';
     };
 
@@ -121,7 +121,7 @@ in
     environment.sessionVariables = {
       # runtime override supported by multiple libraries e. g. libxkbcommon
       # https://xkbcommon.org/doc/current/group__include-path.html
-      XKB_CONFIG_ROOT = "${xkb_patched}/etc/X11/xkb";
+      XKB_CONFIG_ROOT = config.services.xserver.xkbDir;
     };
 
     services.xserver = {
diff --git a/nixos/modules/services/x11/gdk-pixbuf.nix b/nixos/modules/services/x11/gdk-pixbuf.nix
index 2105224f92f..9c088e4cc42 100644
--- a/nixos/modules/services/x11/gdk-pixbuf.nix
+++ b/nixos/modules/services/x11/gdk-pixbuf.nix
@@ -21,7 +21,7 @@ in
   # loaders.cache based on that and set the environment variable
   # GDK_PIXBUF_MODULE_FILE to point to it.
   config = lib.mkIf (cfg.modulePackages != []) {
-    environment.variables = {
+    environment.sessionVariables = {
       GDK_PIXBUF_MODULE_FILE = "${loadersCache}";
     };
   };
diff --git a/nixos/modules/services/x11/hardware/libinput.nix b/nixos/modules/services/x11/hardware/libinput.nix
index f77036360e0..d2a5b5895e0 100644
--- a/nixos/modules/services/x11/hardware/libinput.nix
+++ b/nixos/modules/services/x11/hardware/libinput.nix
@@ -260,7 +260,10 @@ in {
   options = {
 
     services.xserver.libinput = {
-      enable = mkEnableOption (lib.mdDoc "libinput");
+      enable = mkEnableOption (lib.mdDoc "libinput") // {
+        default = config.services.xserver.enable;
+        defaultText = lib.literalExpression "config.services.xserver.enable";
+      };
       mouse = mkConfigForDevice "mouse";
       touchpad = mkConfigForDevice "touchpad";
     };
diff --git a/nixos/modules/services/x11/picom.nix b/nixos/modules/services/x11/picom.nix
index 4a0578de09c..1d6f3daa402 100644
--- a/nixos/modules/services/x11/picom.nix
+++ b/nixos/modules/services/x11/picom.nix
@@ -41,7 +41,7 @@ let
 in {
 
   imports = [
-    (mkAliasOptionModule [ "services" "compton" ] [ "services" "picom" ])
+    (mkAliasOptionModuleMD [ "services" "compton" ] [ "services" "picom" ])
     (mkRemovedOptionModule [ "services" "picom" "refreshRate" ] ''
       This option corresponds to `refresh-rate`, which has been unused
       since picom v6 and was subsequently removed by upstream.
diff --git a/nixos/modules/services/x11/window-managers/default.nix b/nixos/modules/services/x11/window-managers/default.nix
index 48b413beaa8..ce1d4115f22 100644
--- a/nixos/modules/services/x11/window-managers/default.nix
+++ b/nixos/modules/services/x11/window-managers/default.nix
@@ -14,6 +14,7 @@ in
     ./bspwm.nix
     ./cwm.nix
     ./clfswm.nix
+    ./dk.nix
     ./dwm.nix
     ./e16.nix
     ./evilwm.nix
diff --git a/nixos/modules/services/x11/window-managers/dk.nix b/nixos/modules/services/x11/window-managers/dk.nix
new file mode 100644
index 00000000000..152c7bc8117
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/dk.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.xserver.windowManager.dk;
+in
+
+{
+  options = {
+    services.xserver.windowManager.dk = {
+      enable = lib.mkEnableOption (lib.mdDoc "dk");
+
+      package = lib.mkPackageOptionMD pkgs "dk" { };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.xserver.windowManager.session = lib.singleton {
+      name = "dk";
+      start = ''
+        export _JAVA_AWT_WM_NONREPARENTING=1
+        ${cfg.package}/bin/dk &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/herbstluftwm.nix b/nixos/modules/services/x11/window-managers/herbstluftwm.nix
index 816cbb36caf..93705ada116 100644
--- a/nixos/modules/services/x11/window-managers/herbstluftwm.nix
+++ b/nixos/modules/services/x11/window-managers/herbstluftwm.nix
@@ -40,7 +40,7 @@ in
             (cfg.configFile != null)
             ''-c "${cfg.configFile}"''
             ;
-        in "${cfg.package}/bin/herbstluftwm ${configFileClause}";
+        in "${cfg.package}/bin/herbstluftwm ${configFileClause} &";
     };
     environment.systemPackages = [ cfg.package ];
   };
diff --git a/nixos/modules/services/x11/window-managers/katriawm.nix b/nixos/modules/services/x11/window-managers/katriawm.nix
index 106631792ff..9a3fd5f3ca4 100644
--- a/nixos/modules/services/x11/window-managers/katriawm.nix
+++ b/nixos/modules/services/x11/window-managers/katriawm.nix
@@ -1,7 +1,7 @@
 { config, lib, pkgs, ... }:
 
 let
-  inherit (lib) mdDoc mkEnableOption mkIf mkPackageOption singleton;
+  inherit (lib) mdDoc mkEnableOption mkIf mkPackageOptionMD singleton;
   cfg = config.services.xserver.windowManager.katriawm;
 in
 {
@@ -9,7 +9,7 @@ in
   options = {
     services.xserver.windowManager.katriawm = {
       enable = mkEnableOption (mdDoc "katriawm");
-      package = mkPackageOption pkgs "katriawm" {};
+      package = mkPackageOptionMD pkgs "katriawm" {};
     };
   };
 
diff --git a/nixos/modules/services/x11/window-managers/nimdow.nix b/nixos/modules/services/x11/window-managers/nimdow.nix
new file mode 100644
index 00000000000..de319287602
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/nimdow.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.nimdow;
+in
+{
+  options = {
+    services.xserver.windowManager.nimdow.enable = mkEnableOption (lib.mdDoc "nimdow");
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "nimdow";
+      start = ''
+        ${pkgs.nimdow}/bin/nimdow &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.nimdow ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/qtile.nix b/nixos/modules/services/x11/window-managers/qtile.nix
index 523642591d9..a362d5cdbee 100644
--- a/nixos/modules/services/x11/window-managers/qtile.nix
+++ b/nixos/modules/services/x11/window-managers/qtile.nix
@@ -1,23 +1,62 @@
-{ config, lib, pkgs, ... }:
+{ config, pkgs, lib, ... }:
 
 with lib;
 
 let
   cfg = config.services.xserver.windowManager.qtile;
+  pyEnv = pkgs.python3.withPackages (p: [ (cfg.package.unwrapped or cfg.package) ] ++ (cfg.extraPackages p));
 in
 
 {
   options.services.xserver.windowManager.qtile = {
     enable = mkEnableOption (lib.mdDoc "qtile");
 
-    package = mkPackageOption pkgs "qtile" { };
+    package = mkPackageOptionMD pkgs "qtile-unwrapped" { };
+
+    configFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = literalExpression "./your_config.py";
+      description = lib.mdDoc ''
+          Path to the qtile configuration file.
+          If null, $XDG_CONFIG_HOME/qtile/config.py will be used.
+      '';
+    };
+
+    backend = mkOption {
+      type = types.enum [ "x11" "wayland" ];
+      default = "x11";
+      description = lib.mdDoc ''
+          Backend to use in qtile: `x11` or `wayland`.
+      '';
+    };
+
+    extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
+        default = _: [];
+        defaultText = literalExpression ''
+          python3Packages: with python3Packages; [];
+        '';
+        description = lib.mdDoc ''
+          Extra Python packages available to Qtile.
+          An example would be to include `python3Packages.qtile-extras`
+          for additional unofficial widgets.
+        '';
+        example = literalExpression ''
+          python3Packages: with python3Packages; [
+            qtile-extras
+          ];
+        '';
+      };
   };
 
   config = mkIf cfg.enable {
     services.xserver.windowManager.session = [{
       name = "qtile";
       start = ''
-        ${cfg.package}/bin/qtile start &
+        ${pyEnv}/bin/qtile start -b ${cfg.backend} \
+        ${optionalString (cfg.configFile != null)
+        "--config \"${cfg.configFile}\""} &
         waitPID=$!
       '';
     }];
diff --git a/nixos/modules/services/x11/window-managers/stumpwm.nix b/nixos/modules/services/x11/window-managers/stumpwm.nix
index 162af689dbb..c6fc49f5821 100644
--- a/nixos/modules/services/x11/window-managers/stumpwm.nix
+++ b/nixos/modules/services/x11/window-managers/stumpwm.nix
@@ -15,10 +15,10 @@ in
     services.xserver.windowManager.session = singleton {
       name = "stumpwm";
       start = ''
-        ${pkgs.lispPackages.stumpwm}/bin/stumpwm &
+        ${pkgs.sbclPackages.stumpwm}/bin/stumpwm &
         waitPID=$!
       '';
     };
-    environment.systemPackages = [ pkgs.lispPackages.stumpwm ];
+    environment.systemPackages = [ pkgs.sbclPackages.stumpwm ];
   };
 }
diff --git a/nixos/modules/services/x11/xserver.nix b/nixos/modules/services/x11/xserver.nix
index 83a71dcf23e..53d9c99b47d 100644
--- a/nixos/modules/services/x11/xserver.nix
+++ b/nixos/modules/services/x11/xserver.nix
@@ -121,7 +121,7 @@ let
           fi
         done
 
-        for i in $(find ${toString cfg.modules} -type d); do
+        for i in $(find ${toString cfg.modules} -type d | sort); do
           if test $(echo $i/*.so* | wc -w) -ne 0; then
             echo "  ModulePath \"$i\"" >> $out
           fi
@@ -138,6 +138,26 @@ let
     concatMapStringsSep "\n" (line: prefix + line) (splitString "\n" str);
 
   indent = prefixStringLines "  ";
+
+  # A scalable variant of the X11 "core" cursor
+  #
+  # If not running a fancy desktop environment, the cursor is likely set to
+  # the default `cursor.pcf` bitmap font. This is 17px wide, so it's very
+  # small and almost invisible on 4K displays.
+  fontcursormisc_hidpi = pkgs.xorg.fontxfree86type1.overrideAttrs (old:
+    let
+      # The scaling constant is 230/96: the scalable `left_ptr` glyph at
+      # about 23 points is rendered as 17px, on a 96dpi display.
+      # Note: the XLFD font size is in decipoints.
+      size = 2.39583 * cfg.dpi;
+      sizeString = builtins.head (builtins.split "\\." (toString size));
+    in
+    {
+      postInstall = ''
+        alias='cursor -xfree86-cursor-medium-r-normal--0-${sizeString}-0-0-p-0-adobe-fontspecific'
+        echo "$alias" > $out/lib/X11/fonts/Type1/fonts.alias
+      '';
+    });
 in
 
 {
@@ -256,7 +276,7 @@ in
 
       videoDrivers = mkOption {
         type = types.listOf types.str;
-        default = [ "amdgpu" "radeon" "nouveau" "modesetting" "fbdev" ];
+        default = [ "modesetting" "fbdev" ];
         example = [
           "nvidia" "nvidiaLegacy390" "nvidiaLegacy340" "nvidiaLegacy304"
           "amdgpu-pro"
@@ -576,6 +596,15 @@ in
           Whether to terminate X upon server reset.
         '';
       };
+
+      upscaleDefaultCursor = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Upscale the default X cursor to be more visible on high-density displays.
+          Requires `config.services.xserver.dpi` to be set.
+        '';
+      };
     };
 
   };
@@ -592,7 +621,8 @@ in
                     || dmConf.sddm.enable
                     || dmConf.xpra.enable
                     || dmConf.sx.enable
-                    || dmConf.startx.enable);
+                    || dmConf.startx.enable
+                    || config.services.greetd.enable);
       in mkIf (default) (mkDefault true);
 
     # so that the service won't be enabled when only startx is used
@@ -626,6 +656,10 @@ in
                 + "${toString (length primaryHeads)} heads set to primary: "
                 + concatMapStringsSep ", " (x: x.output) primaryHeads;
       })
+      {
+        assertion = cfg.upscaleDefaultCursor -> cfg.dpi != null;
+        message = "Specify `config.services.xserver.dpi` to upscale the default cursor.";
+      }
     ];
 
     environment.etc =
@@ -691,7 +725,7 @@ in
     systemd.defaultUnit = mkIf cfg.autorun "graphical.target";
 
     systemd.services.display-manager =
-      { description = "X11 Server";
+      { description = "Display Manager";
 
         after = [ "acpid.service" "systemd-logind.service" "systemd-user-sessions.service" ];
 
@@ -742,7 +776,7 @@ in
         xorg.xf86inputevdev.out
       ];
 
-    system.extraDependencies = singleton (pkgs.runCommand "xkb-validated" {
+    system.checks = singleton (pkgs.runCommand "xkb-validated" {
       inherit (cfg) xkbModel layout xkbVariant xkbOptions;
       nativeBuildInputs = with pkgs.buildPackages; [ xkbvalidate ];
       preferLocalBuild = true;
@@ -850,6 +884,10 @@ in
       '';
 
     fonts.enableDefaultFonts = mkDefault true;
+    fonts.fonts = [
+      (if cfg.upscaleDefaultCursor then fontcursormisc_hidpi else pkgs.xorg.fontcursormisc)
+      pkgs.xorg.fontmiscmisc
+    ];
 
   };
 
diff --git a/nixos/modules/system/activation/activatable-system.nix b/nixos/modules/system/activation/activatable-system.nix
new file mode 100644
index 00000000000..7f6154794bd
--- /dev/null
+++ b/nixos/modules/system/activation/activatable-system.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib)
+    mkOption
+    optionalString
+    types
+    ;
+
+  perlWrapped = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
+
+  systemBuilderArgs = {
+    activationScript = config.system.activationScripts.script;
+    dryActivationScript = config.system.dryActivationScript;
+  };
+
+  systemBuilderCommands = ''
+    echo "$activationScript" > $out/activate
+    echo "$dryActivationScript" > $out/dry-activate
+    substituteInPlace $out/activate --subst-var-by out ''${!toplevelVar}
+    substituteInPlace $out/dry-activate --subst-var-by out ''${!toplevelVar}
+    chmod u+x $out/activate $out/dry-activate
+    unset activationScript dryActivationScript
+
+    mkdir $out/bin
+    substitute ${./switch-to-configuration.pl} $out/bin/switch-to-configuration \
+      --subst-var out \
+      --subst-var-by toplevel ''${!toplevelVar} \
+      --subst-var-by coreutils "${pkgs.coreutils}" \
+      --subst-var-by distroId ${lib.escapeShellArg config.system.nixos.distroId} \
+      --subst-var-by installBootLoader ${lib.escapeShellArg config.system.build.installBootLoader} \
+      --subst-var-by localeArchive "${config.i18n.glibcLocales}/lib/locale/locale-archive" \
+      --subst-var-by perl "${perlWrapped}" \
+      --subst-var-by shell "${pkgs.bash}/bin/sh" \
+      --subst-var-by su "${pkgs.shadow.su}/bin/su" \
+      --subst-var-by systemd "${config.systemd.package}" \
+      --subst-var-by utillinux "${pkgs.util-linux}" \
+      ;
+
+    chmod +x $out/bin/switch-to-configuration
+    ${optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
+      if ! output=$(${perlWrapped}/bin/perl -c $out/bin/switch-to-configuration 2>&1); then
+        echo "switch-to-configuration syntax is not valid:"
+        echo "$output"
+        exit 1
+      fi
+    ''}
+  '';
+
+in
+{
+  options = {
+    system.activatable = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to add the activation script to the system profile.
+
+        The default, to have the script available all the time, is what we normally
+        do, but for image based systems, this may not be needed or not be desirable.
+      '';
+    };
+    system.build.separateActivationScript = mkOption {
+      type = types.package;
+      description = ''
+        A separate activation script package that's not part of the system profile.
+
+        This is useful for configurations where `system.activatable` is `false`.
+        Otherwise, you can just use `system.build.toplevel`.
+      '';
+    };
+  };
+  config = {
+    system.systemBuilderCommands = lib.mkIf config.system.activatable systemBuilderCommands;
+    system.systemBuilderArgs = lib.mkIf config.system.activatable
+      (systemBuilderArgs // {
+        toplevelVar = "out";
+      });
+
+    system.build.separateActivationScript =
+      pkgs.runCommand
+        "separate-activation-script"
+        (systemBuilderArgs // {
+          toplevelVar = "toplevel";
+          toplevel = config.system.build.toplevel;
+        })
+        ''
+          mkdir $out
+          ${systemBuilderCommands}
+        '';
+  };
+}
diff --git a/nixos/modules/system/activation/activation-script.nix b/nixos/modules/system/activation/activation-script.nix
index ddb165a76cc..c8407dd6779 100644
--- a/nixos/modules/system/activation/activation-script.nix
+++ b/nixos/modules/system/activation/activation-script.nix
@@ -204,6 +204,27 @@ in
         `/usr/bin/env`.
       '';
     };
+
+    system.build.installBootLoader = mkOption {
+      internal = true;
+      # "; true" => make the `$out` argument from switch-to-configuration.pl
+      #             go to `true` instead of `echo`, hiding the useless path
+      #             from the log.
+      default = "echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2; true";
+      description = lib.mdDoc ''
+        A program that writes a bootloader installation script to the path passed in the first command line argument.
+
+        See `nixos/modules/system/activation/switch-to-configuration.pl`.
+      '';
+      type = types.unique {
+        message = ''
+          Only one bootloader can be enabled at a time. This requirement has not
+          been checked until NixOS 22.05. Earlier versions defaulted to the last
+          definition. Change your configuration to enable only one bootloader.
+        '';
+      } (types.either types.str types.package);
+    };
+
   };
 
 
@@ -217,7 +238,8 @@ in
       ''
         # Various log/runtime directories.
 
-        mkdir -m 1777 -p /var/tmp
+        mkdir -p /var/tmp
+        chmod 1777 /var/tmp
 
         # Empty, immutable home directory of many system accounts.
         mkdir -p /var/empty
@@ -231,7 +253,8 @@ in
 
     system.activationScripts.usrbinenv = if config.environment.usrbinenv != null
       then ''
-        mkdir -m 0755 -p /usr/bin
+        mkdir -p /usr/bin
+        chmod 0755 /usr/bin
         ln -sfn ${config.environment.usrbinenv} /usr/bin/.env.tmp
         mv /usr/bin/.env.tmp /usr/bin/env # atomically replace /usr/bin/env
       ''
@@ -251,7 +274,8 @@ in
           if mountpoint -q "$mountPoint"; then
             local options="remount,$options"
           else
-            mkdir -m 0755 -p "$mountPoint"
+            mkdir -p "$mountPoint"
+            chmod 0755 "$mountPoint"
           fi
           mount -t "$fsType" -o "$options" "$device" "$mountPoint"
         }
diff --git a/nixos/modules/system/activation/bootspec.cue b/nixos/modules/system/activation/bootspec.cue
index 9f857a1b1cd..1f7b4afa87a 100644
--- a/nixos/modules/system/activation/bootspec.cue
+++ b/nixos/modules/system/activation/bootspec.cue
@@ -1,4 +1,6 @@
-#V1: {
+import "struct"
+
+#BootspecV1: {
 	system:         string
 	init:           string
 	initrd?:        string
@@ -7,12 +9,23 @@
 	kernelParams: [...string]
 	label:    string
 	toplevel: string
-	specialisation?: {
-		[=~"^"]: #V1
-	}
-	extensions?: {...}
 }
 
-Document: {
-	v1: #V1
+// A restricted document does not allow any official specialisation
+// information in it to avoid "recursive specialisations".
+#RestrictedDocument: struct.MinFields(1) & {
+	"org.nixos.bootspec.v1": #BootspecV1
+	[=~"^"]:                 #BootspecExtension
+}
+
+// Specialisations are a hashmap of strings
+#BootspecSpecialisationV1: [string]: #RestrictedDocument
+
+// Bootspec extensions are defined by the extension author.
+#BootspecExtension: {...}
+
+// A "full" document allows official specialisation information
+// in the top-level with a reserved namespaced key.
+Document: #RestrictedDocument & {
+	"org.nixos.specialisation.v1"?: #BootspecSpecialisationV1
 }
diff --git a/nixos/modules/system/activation/bootspec.nix b/nixos/modules/system/activation/bootspec.nix
index 61407ab6755..9e1fa309d5d 100644
--- a/nixos/modules/system/activation/bootspec.nix
+++ b/nixos/modules/system/activation/bootspec.nix
@@ -16,20 +16,20 @@ let
       filename = "boot.json";
       json =
         pkgs.writeText filename
-          (builtins.toJSON
+        (builtins.toJSON
+          # Merge extensions first to not let them shadow NixOS bootspec data.
+          (cfg.extensions //
           {
-            v1 = {
+            "org.nixos.bootspec.v1" = {
               system = config.boot.kernelPackages.stdenv.hostPlatform.system;
               kernel = "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}";
               kernelParams = config.boot.kernelParams;
-              label = "NixOS ${config.system.nixos.codeName} ${config.system.nixos.label} (Linux ${config.boot.kernelPackages.kernel.modDirVersion})";
-
-              inherit (cfg) extensions;
+              label = "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} (Linux ${config.boot.kernelPackages.kernel.modDirVersion})";
             } // lib.optionalAttrs config.boot.initrd.enable {
               initrd = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
               initrdSecrets = "${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets";
             };
-          });
+          }));
 
       generator =
         let
@@ -40,10 +40,10 @@ let
           # This can only be done here because we *cannot* depend on $out
           # referring to the toplevel, except by living in the toplevel itself.
           toplevelInjector = lib.escapeShellArgs [
-            "${pkgs.jq}/bin/jq"
+            "${pkgs.buildPackages.jq}/bin/jq"
             ''
-              .v1.toplevel = $toplevel |
-              .v1.init = $init
+              ."org.nixos.bootspec.v1".toplevel = $toplevel |
+              ."org.nixos.bootspec.v1".init = $init
             ''
             "--sort-keys"
             "--arg" "toplevel" "${placeholder "out"}"
@@ -60,16 +60,12 @@ let
                 children);
             in
             lib.escapeShellArgs [
-              "${pkgs.jq}/bin/jq"
+              "${pkgs.buildPackages.jq}/bin/jq"
               "--sort-keys"
-              ".v1.specialisation = ($ARGS.named | map_values(. | first | .v1))"
+              ''."org.nixos.specialisation.v1" = ($ARGS.named | map_values(. | first))''
             ] + " ${lib.concatStringsSep " " specialisationLoader}";
         in
-        ''
-          mkdir -p $out/bootspec
-
-          ${toplevelInjector} | ${specialisationInjector} > $out/${filename}
-        '';
+        "${toplevelInjector} | ${specialisationInjector} > $out/${filename}";
 
       validator = pkgs.writeCueValidator ./bootspec.cue {
         document = "Document"; # Universal validator for any version as long the schema is correctly set.
@@ -79,10 +75,17 @@ let
 in
 {
   options.boot.bootspec = {
-    enable = lib.mkEnableOption (lib.mdDoc "Enable generation of RFC-0125 bootspec in $system/bootspec, e.g. /run/current-system/bootspec");
+    enable = lib.mkEnableOption (lib.mdDoc "the generation of RFC-0125 bootspec in $system/boot.json, e.g. /run/current-system/boot.json")
+      // { default = true; internal = true; };
+    enableValidation = lib.mkEnableOption (lib.mdDoc ''the validation of bootspec documents for each build.
+      This will introduce Go in the build-time closure as we are relying on [Cuelang](https://cuelang.org/) for schema validation.
+      Enable this option if you want to ascertain that your documents are correct.
+      ''
+    );
 
     extensions = lib.mkOption {
-      type = lib.types.attrsOf lib.types.attrs; # <namespace>: { ...namespace-specific fields }
+      # NOTE(RaitoBezarius): this is not enough to validate: extensions."osRelease" = drv; those are picked up by cue validation.
+      type = lib.types.attrsOf lib.types.anything; # <namespace>: { ...namespace-specific fields }
       default = { };
       description = lib.mdDoc ''
         User-defined data that extends the bootspec document.
@@ -112,15 +115,4 @@ in
       default = schemas.v1.filename;
     };
   };
-
-  config = lib.mkIf (cfg.enable) {
-    warnings = [
-      ''RFC-0125 is not merged yet, this is a feature preview of bootspec.
-        The schema is not definitive and features are not guaranteed to be stable until RFC-0125 is merged.
-        See:
-        - https://github.com/NixOS/nixpkgs/pull/172237 to track merge status in nixpkgs.
-        - https://github.com/NixOS/rfcs/pull/125 to track RFC status.
-      ''
-    ];
-  };
 }
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index 9a4c635402d..cfad6403986 100755
--- a/nixos/modules/system/activation/switch-to-configuration.pl
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -31,8 +31,10 @@ use Cwd qw(abs_path);
 ## no critic(ValuesAndExpressions::ProhibitNoisyQuotes, ValuesAndExpressions::ProhibitMagicNumbers, ValuesAndExpressions::ProhibitEmptyQuotes, ValuesAndExpressions::ProhibitInterpolationOfLiterals)
 ## no critic(RegularExpressions::ProhibitEscapedMetacharacters)
 
-# System closure path to switch to
+# Location of activation scripts
 my $out = "@out@";
+# System closure path to switch to
+my $toplevel = "@toplevel@";
 # Path to the directory containing systemd tools of the old system
 my $cur_systemd = abs_path("/run/current-system/sw/bin");
 # Path to the systemd store path of the new system
@@ -84,7 +86,7 @@ EOF
 
 # This is a NixOS installation if it has /etc/NIXOS or a proper
 # /etc/os-release.
-if (!-f "/etc/NIXOS" && (read_file("/etc/os-release", err_mode => "quiet") // "") !~ /^ID="?nixos"?/msx) {
+if (!-f "/etc/NIXOS" && (read_file("/etc/os-release", err_mode => "quiet") // "") !~ /^ID="?@distroId@"?/msx) {
     die("This is not a NixOS installation!\n");
 }
 
@@ -96,7 +98,7 @@ if ($action eq "switch" || $action eq "boot") {
     chomp(my $install_boot_loader = <<'EOFBOOTLOADER');
 @installBootLoader@
 EOFBOOTLOADER
-    system("$install_boot_loader $out") == 0 or exit 1;
+    system("$install_boot_loader $toplevel") == 0 or exit 1;
 }
 
 # Just in case the new configuration hangs the system, do a sync now.
@@ -110,7 +112,7 @@ if ($action eq "boot") {
 
 # Check if we can activate the new configuration.
 my $cur_init_interface_version = read_file("/run/current-system/init-interface-version", err_mode => "quiet") // "";
-my $new_init_interface_version = read_file("$out/init-interface-version");
+my $new_init_interface_version = read_file("$toplevel/init-interface-version");
 
 if ($new_init_interface_version ne $cur_init_interface_version) {
     print STDERR <<'EOF';
@@ -477,7 +479,7 @@ sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutin
                             $units_to_stop->{$socket} = 1;
                             # Only restart sockets that actually
                             # exist in new configuration:
-                            if (-e "$out/etc/systemd/system/$socket") {
+                            if (-e "$toplevel/etc/systemd/system/$socket") {
                                 $units_to_start->{$socket} = 1;
                                 if ($units_to_start eq $units_to_restart) {
                                     record_unit($restart_list_file, $socket);
@@ -539,13 +541,13 @@ while (my ($unit, $state) = each(%{$active_cur})) {
     my $base_unit = $unit;
 
     my $cur_unit_file = "/etc/systemd/system/$base_unit";
-    my $new_unit_file = "$out/etc/systemd/system/$base_unit";
+    my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
 
     # Detect template instances.
     if (!-e $cur_unit_file && !-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
       $base_unit = "$1\@.$2";
       $cur_unit_file = "/etc/systemd/system/$base_unit";
-      $new_unit_file = "$out/etc/systemd/system/$base_unit";
+      $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
     }
 
     my $base_name = $base_unit;
@@ -626,7 +628,7 @@ sub path_to_unit_name {
 # we generated units for all mounts; then we could unify this with the
 # unit checking code above.
 my ($cur_fss, $cur_swaps) = parse_fstab("/etc/fstab");
-my ($new_fss, $new_swaps) = parse_fstab("$out/etc/fstab");
+my ($new_fss, $new_swaps) = parse_fstab("$toplevel/etc/fstab");
 foreach my $mount_point (keys(%{$cur_fss})) {
     my $cur = $cur_fss->{$mount_point};
     my $new = $new_fss->{$mount_point};
@@ -655,7 +657,7 @@ foreach my $device (keys(%{$cur_swaps})) {
         # "systemctl stop" here because systemd has lots of alias
         # units that prevent a stop from actually calling
         # "swapoff".
-        if ($action ne "dry-activate") {
+        if ($action eq "dry-activate") {
             print STDERR "would stop swap device: $device\n";
         } else {
             print STDERR "stopping swap device: $device\n";
@@ -670,7 +672,7 @@ foreach my $device (keys(%{$cur_swaps})) {
 my $cur_pid1_path = abs_path("/proc/1/exe") // "/unknown";
 my $cur_systemd_system_config = abs_path("/etc/systemd/system.conf") // "/unknown";
 my $new_pid1_path = abs_path("$new_systemd/lib/systemd/systemd") or die;
-my $new_systemd_system_config = abs_path("$out/etc/systemd/system.conf") // "/unknown";
+my $new_systemd_system_config = abs_path("$toplevel/etc/systemd/system.conf") // "/unknown";
 
 my $restart_systemd = $cur_pid1_path ne $new_pid1_path;
 if ($cur_systemd_system_config ne $new_systemd_system_config) {
@@ -709,12 +711,12 @@ if ($action eq "dry-activate") {
     foreach (split(/\n/msx, read_file($dry_restart_by_activation_file, err_mode => "quiet") // "")) {
         my $unit = $_;
         my $base_unit = $unit;
-        my $new_unit_file = "$out/etc/systemd/system/$base_unit";
+        my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
 
         # Detect template instances.
         if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
           $base_unit = "$1\@.$2";
-          $new_unit_file = "$out/etc/systemd/system/$base_unit";
+          $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
         }
 
         my $base_name = $base_unit;
@@ -757,7 +759,7 @@ if ($action eq "dry-activate") {
 }
 
 
-syslog(LOG_NOTICE, "switching to system configuration $out");
+syslog(LOG_NOTICE, "switching to system configuration $toplevel");
 
 if (scalar(keys(%units_to_stop)) > 0) {
     if (scalar(@units_to_stop_filtered)) {
@@ -781,12 +783,12 @@ system("$out/activate", "$out") == 0 or $res = 2;
 foreach (split(/\n/msx, read_file($restart_by_activation_file, err_mode => "quiet") // "")) {
     my $unit = $_;
     my $base_unit = $unit;
-    my $new_unit_file = "$out/etc/systemd/system/$base_unit";
+    my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
 
     # Detect template instances.
     if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
       $base_unit = "$1\@.$2";
-      $new_unit_file = "$out/etc/systemd/system/$base_unit";
+      $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
     }
 
     my $base_name = $base_unit;
@@ -857,7 +859,7 @@ if (scalar(keys(%units_to_reload)) > 0) {
     for my $unit (keys(%units_to_reload)) {
         if (!unit_is_active($unit)) {
             # Figure out if we need to start the unit
-            my %unit_info = parse_unit("$out/etc/systemd/system/$unit");
+            my %unit_info = parse_unit("$toplevel/etc/systemd/system/$unit");
             if (!(parse_systemd_bool(\%unit_info, "Unit", "RefuseManualStart", 0) || parse_systemd_bool(\%unit_info, "Unit", "X-OnlyManualStart", 0))) {
                 $units_to_start{$unit} = 1;
                 record_unit($start_list_file, $unit);
@@ -940,9 +942,9 @@ if (scalar(@failed) > 0) {
 }
 
 if ($res == 0) {
-    syslog(LOG_NOTICE, "finished switching to system configuration $out");
+    syslog(LOG_NOTICE, "finished switching to system configuration $toplevel");
 } else {
-    syslog(LOG_ERR, "switching to system configuration $out failed (status $res)");
+    syslog(LOG_ERR, "switching to system configuration $toplevel failed (status $res)");
 }
 
 exit($res);
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index 00b11471e1c..07c2e05ce37 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -36,13 +36,6 @@ let
         ln -s ${config.hardware.firmware}/lib/firmware $out/firmware
       ''}
 
-      echo "$activationScript" > $out/activate
-      echo "$dryActivationScript" > $out/dry-activate
-      substituteInPlace $out/activate --subst-var out
-      substituteInPlace $out/dry-activate --subst-var out
-      chmod u+x $out/activate $out/dry-activate
-      unset activationScript dryActivationScript
-
       ${if config.boot.initrd.systemd.enable then ''
         cp ${config.system.build.bootStage2} $out/prepare-root
         substituteInPlace $out/prepare-root --subst-var-by systemConfig $out
@@ -63,25 +56,14 @@ let
       echo -n "$nixosLabel" > $out/nixos-version
       echo -n "${config.boot.kernelPackages.stdenv.hostPlatform.system}" > $out/system
 
-      mkdir $out/bin
-      export localeArchive="${config.i18n.glibcLocales}/lib/locale/locale-archive"
-      substituteAll ${./switch-to-configuration.pl} $out/bin/switch-to-configuration
-      chmod +x $out/bin/switch-to-configuration
-      ${optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
-        if ! output=$($perl/bin/perl -c $out/bin/switch-to-configuration 2>&1); then
-          echo "switch-to-configuration syntax is not valid:"
-          echo "$output"
-          exit 1
-        fi
-      ''}
-
       ${config.system.systemBuilderCommands}
 
-      echo -n "$extraDependencies" > $out/extra-dependencies
+      cp "$extraDependenciesPath" "$out/extra-dependencies"
 
       ${optionalString (!config.boot.isContainer && config.boot.bootspec.enable) ''
         ${config.boot.bootspec.writer}
-        ${config.boot.bootspec.validator} "$out/${config.boot.bootspec.filename}"
+        ${optionalString config.boot.bootspec.enableValidation
+          ''${config.boot.bootspec.validator} "$out/${config.boot.bootspec.filename}"''}
       ''}
 
       ${config.system.extraSystemBuilderCmds}
@@ -91,29 +73,20 @@ let
   # symlinks to the various parts of the built configuration (the
   # kernel, systemd units, init scripts, etc.) as well as a script
   # `switch-to-configuration' that activates the configuration and
-  # makes it bootable.
+  # makes it bootable. See `activatable-system.nix`.
   baseSystem = pkgs.stdenvNoCC.mkDerivation ({
     name = "nixos-system-${config.system.name}-${config.system.nixos.label}";
     preferLocalBuild = true;
     allowSubstitutes = false;
+    passAsFile = [ "extraDependencies" ];
     buildCommand = systemBuilder;
 
-    inherit (pkgs) coreutils;
     systemd = config.systemd.package;
-    shell = "${pkgs.bash}/bin/sh";
-    su = "${pkgs.shadow.su}/bin/su";
-    utillinux = pkgs.util-linux;
 
     kernelParams = config.boot.kernelParams;
-    installBootLoader = config.system.build.installBootLoader;
-    activationScript = config.system.activationScripts.script;
-    dryActivationScript = config.system.dryActivationScript;
     nixosLabel = config.system.nixos.label;
 
     inherit (config.system) extraDependencies;
-
-    # Needed by switch-to-configuration.
-    perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
   } // config.system.systemBuilderArgs);
 
   # Handle assertions and warnings
@@ -129,6 +102,13 @@ let
       pkgs.replaceDependency { inherit oldDependency newDependency drv; }
     ) baseSystemAssertWarn config.system.replaceRuntimeDependencies;
 
+  systemWithBuildDeps = system.overrideAttrs (o: {
+    systemBuildClosure = pkgs.closureInfo { rootPaths = [ system.drvPath ]; };
+    buildCommand = o.buildCommand + ''
+      ln -sn $systemBuildClosure $out/build-closure
+    '';
+  });
+
 in
 
 {
@@ -168,26 +148,6 @@ in
     };
 
     system.build = {
-      installBootLoader = mkOption {
-        internal = true;
-        # "; true" => make the `$out` argument from switch-to-configuration.pl
-        #             go to `true` instead of `echo`, hiding the useless path
-        #             from the log.
-        default = "echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2; true";
-        description = lib.mdDoc ''
-          A program that writes a bootloader installation script to the path passed in the first command line argument.
-
-          See `nixos/modules/system/activation/switch-to-configuration.pl`.
-        '';
-        type = types.unique {
-          message = ''
-            Only one bootloader can be enabled at a time. This requirement has not
-            been checked until NixOS 22.05. Earlier versions defaulted to the last
-            definition. Change your configuration to enable only one bootloader.
-          '';
-        } (types.either types.str types.package);
-      };
-
       toplevel = mkOption {
         type = types.package;
         readOnly = true;
@@ -250,12 +210,27 @@ in
     };
 
     system.extraDependencies = mkOption {
+      type = types.listOf types.pathInStore;
+      default = [];
+      description = lib.mdDoc ''
+        A list of paths that should be included in the system
+        closure but generally not visible to users.
+
+        This option has also been used for build-time checks, but the
+        `system.checks` option is more appropriate for that purpose as checks
+        should not leave a trace in the built system configuration.
+      '';
+    };
+
+    system.checks = mkOption {
       type = types.listOf types.package;
       default = [];
       description = lib.mdDoc ''
-        A list of packages that should be included in the system
-        closure but not otherwise made available to users. This is
-        primarily used by the installation tests.
+        Packages that are added as dependencies of the system's build, usually
+        for the purpose of validating some part of the configuration.
+
+        Unlike `system.extraDependencies`, these store paths do not
+        become part of the built system configuration.
       '';
     };
 
@@ -305,10 +280,37 @@ in
       '';
     };
 
+    system.includeBuildDependencies = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Whether to include the build closure of the whole system in
+        its runtime closure.  This can be useful for making changes
+        fully offline, as it includes all sources, patches, and
+        intermediate outputs required to build all the derivations
+        that the system depends on.
+
+        Note that this includes _all_ the derivations, down from the
+        included applications to their sources, the compilers used to
+        build them, and even the bootstrap compiler used to compile
+        the compilers. This increases the size of the system and the
+        time needed to download its dependencies drastically: a
+        minimal configuration with no extra services enabled grows
+        from ~670MiB in size to 13.5GiB, and takes proportionally
+        longer to download.
+      '';
+    };
+
   };
 
 
   config = {
+    assertions = [
+      {
+        assertion = config.system.copySystemConfiguration -> !lib.inPureEvalMode;
+        message = "system.copySystemConfiguration is not supported with flakes";
+      }
+    ];
 
     system.extraSystemBuilderCmds =
       optionalString
@@ -327,7 +329,27 @@ in
           fi
         '';
 
-    system.systemBuilderArgs = lib.optionalAttrs (config.system.forbiddenDependenciesRegex != "") {
+    system.systemBuilderArgs = {
+
+      # Legacy environment variables. These were used by the activation script,
+      # but some other script might still depend on them, although unlikely.
+      installBootLoader = config.system.build.installBootLoader;
+      localeArchive = "${config.i18n.glibcLocales}/lib/locale/locale-archive";
+      distroId = config.system.nixos.distroId;
+      perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
+      # End if legacy environment variables
+
+
+      # Not actually used in the builder. `passedChecks` is just here to create
+      # the build dependencies. Checks are similar to build dependencies in the
+      # sense that if they fail, the system build fails. However, checks do not
+      # produce any output of value, so they are not used by the system builder.
+      # In fact, using them runs the risk of accidentally adding unneeded paths
+      # to the system closure, which defeats the purpose of the `system.checks`
+      # option, as opposed to `system.extraDependencies`.
+      passedChecks = concatStringsSep " " config.system.checks;
+    }
+    // lib.optionalAttrs (config.system.forbiddenDependenciesRegex != "") {
       inherit (config.system) forbiddenDependenciesRegex;
       closureInfo = pkgs.closureInfo { rootPaths = [
         # override to avoid  infinite recursion (and to allow using extraDependencies to add forbidden dependencies)
@@ -335,7 +357,8 @@ in
       ]; };
     };
 
-    system.build.toplevel = system;
+
+    system.build.toplevel = if config.system.includeBuildDependencies then systemWithBuildDeps else system;
 
   };
 
diff --git a/nixos/modules/system/boot/binfmt.nix b/nixos/modules/system/boot/binfmt.nix
index 7f817e5d350..bf1688feb19 100644
--- a/nixos/modules/system/boot/binfmt.nix
+++ b/nixos/modules/system/boot/binfmt.nix
@@ -125,6 +125,10 @@ let
       magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00'';
       mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
     };
+    loongarch64-linux = {
+      magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x02\x01'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\xfc\x00\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+    };
     wasm32-wasi = {
       magicOrExtension = ''\x00asm'';
       mask = ''\xff\xff\xff\xff'';
@@ -134,11 +138,11 @@ let
       mask = ''\xff\xff\xff\xff'';
     };
     x86_64-windows = {
-      magicOrExtension = ".exe";
+      magicOrExtension = "exe";
       recognitionType = "extension";
     };
     i686-windows = {
-      magicOrExtension = ".exe";
+      magicOrExtension = "exe";
       recognitionType = "extension";
     };
   };
@@ -313,14 +317,17 @@ in {
     environment.etc."binfmt.d/nixos.conf".source = builtins.toFile "binfmt_nixos.conf"
       (lib.concatStringsSep "\n" (lib.mapAttrsToList makeBinfmtLine config.boot.binfmt.registrations));
     system.activationScripts.binfmt = stringAfter [ "specialfs" ] ''
-      mkdir -p -m 0755 /run/binfmt
+      mkdir -p /run/binfmt
+      chmod 0755 /run/binfmt
       ${lib.concatStringsSep "\n" (lib.mapAttrsToList activationSnippet config.boot.binfmt.registrations)}
     '';
-    systemd.additionalUpstreamSystemUnits = lib.mkIf (config.boot.binfmt.registrations != {}) [
-      "proc-sys-fs-binfmt_misc.automount"
-      "proc-sys-fs-binfmt_misc.mount"
-      "systemd-binfmt.service"
-    ];
-    systemd.services.systemd-binfmt.restartTriggers = [ (builtins.toJSON config.boot.binfmt.registrations) ];
+    systemd = lib.mkIf (config.boot.binfmt.registrations != {}) {
+      additionalUpstreamSystemUnits = [
+        "proc-sys-fs-binfmt_misc.automount"
+        "proc-sys-fs-binfmt_misc.mount"
+        "systemd-binfmt.service"
+      ];
+      services.systemd-binfmt.restartTriggers = [ (builtins.toJSON config.boot.binfmt.registrations) ];
+    };
   };
 }
diff --git a/nixos/modules/system/boot/grow-partition.nix b/nixos/modules/system/boot/grow-partition.nix
index 034b2b9906f..a2764187a53 100644
--- a/nixos/modules/system/boot/grow-partition.nix
+++ b/nixos/modules/system/boot/grow-partition.nix
@@ -17,6 +17,11 @@ with lib;
 
   config = mkIf config.boot.growPartition {
 
+    assertions = [{
+      assertion = !config.boot.initrd.systemd.enable;
+      message = "systemd stage 1 does not support 'boot.growPartition' yet.";
+    }];
+
     boot.initrd.extraUtilsCommands = ''
       copy_bin_and_libs ${pkgs.gawk}/bin/gawk
       copy_bin_and_libs ${pkgs.gnused}/bin/sed
diff --git a/nixos/modules/system/boot/initrd-network.nix b/nixos/modules/system/boot/initrd-network.nix
index a1017c3e242..e8bbf1d0403 100644
--- a/nixos/modules/system/boot/initrd-network.nix
+++ b/nixos/modules/system/boot/initrd-network.nix
@@ -67,11 +67,15 @@ in
 
     boot.initrd.network.flushBeforeStage2 = mkOption {
       type = types.bool;
-      default = true;
+      default = !config.boot.initrd.systemd.enable;
+      defaultText = "!config.boot.initrd.systemd.enable";
       description = lib.mdDoc ''
         Whether to clear the configuration of the interfaces that were set up in
         the initrd right before stage 2 takes over. Stage 2 will do the regular network
         configuration based on the NixOS networking options.
+
+        The default is false when systemd is enabled in initrd,
+        because the systemd-networkd documentation suggests it.
       '';
     };
 
diff --git a/nixos/modules/system/boot/initrd-openvpn.nix b/nixos/modules/system/boot/initrd-openvpn.nix
index cbc61d55d6b..2530240628e 100644
--- a/nixos/modules/system/boot/initrd-openvpn.nix
+++ b/nixos/modules/system/boot/initrd-openvpn.nix
@@ -51,7 +51,7 @@ in
 
     # Add openvpn and ip binaries to the initrd
     # The shared libraries are required for DNS resolution
-    boot.initrd.extraUtilsCommands = ''
+    boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
       copy_bin_and_libs ${pkgs.openvpn}/bin/openvpn
       copy_bin_and_libs ${pkgs.iproute2}/bin/ip
 
@@ -59,18 +59,33 @@ in
       cp -pv ${pkgs.glibc}/lib/libnss_dns.so.2 $out/lib
     '';
 
+    boot.initrd.systemd.storePaths = [
+      "${pkgs.openvpn}/bin/openvpn"
+      "${pkgs.iproute2}/bin/ip"
+      "${pkgs.glibc}/lib/libresolv.so.2"
+      "${pkgs.glibc}/lib/libnss_dns.so.2"
+    ];
+
     boot.initrd.secrets = {
       "/etc/initrd.ovpn" = cfg.configuration;
     };
 
     # openvpn --version would exit with 1 instead of 0
-    boot.initrd.extraUtilsCommandsTest = ''
+    boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
       $out/bin/openvpn --show-gateway
     '';
 
-    boot.initrd.network.postCommands = ''
+    boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) ''
       openvpn /etc/initrd.ovpn &
     '';
+
+    boot.initrd.systemd.services.openvpn = {
+      wantedBy = [ "initrd.target" ];
+      path = [ pkgs.iproute2 ];
+      after = [ "network.target" "initrd-nixos-copy-secrets.service" ];
+      serviceConfig.ExecStart = "${pkgs.openvpn}/bin/openvpn /etc/initrd.ovpn";
+      serviceConfig.Type = "notify";
+    };
   };
 
 }
diff --git a/nixos/modules/system/boot/initrd-ssh.nix b/nixos/modules/system/boot/initrd-ssh.nix
index 701d242abc1..60c5ff62fff 100644
--- a/nixos/modules/system/boot/initrd-ssh.nix
+++ b/nixos/modules/system/boot/initrd-ssh.nix
@@ -5,6 +5,10 @@ with lib;
 let
 
   cfg = config.boot.initrd.network.ssh;
+  shell = if cfg.shell == null then "/bin/ash" else cfg.shell;
+  inherit (config.programs.ssh) package;
+
+  enabled = let initrd = config.boot.initrd; in (initrd.network.enable || initrd.systemd.network.enable) && cfg.enable;
 
 in
 
@@ -33,8 +37,9 @@ in
     };
 
     shell = mkOption {
-      type = types.str;
-      default = "/bin/ash";
+      type = types.nullOr types.str;
+      default = null;
+      defaultText = ''"/bin/ash"'';
       description = lib.mdDoc ''
         Login shell of the remote user. Can be used to limit actions user can do.
       '';
@@ -119,22 +124,24 @@ in
     sshdCfg = config.services.openssh;
 
     sshdConfig = ''
+      UsePAM no
       Port ${toString cfg.port}
 
       PasswordAuthentication no
+      AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys2 /etc/ssh/authorized_keys.d/%u
       ChallengeResponseAuthentication no
 
       ${flip concatMapStrings cfg.hostKeys (path: ''
         HostKey ${initrdKeyPath path}
       '')}
 
-      KexAlgorithms ${concatStringsSep "," sshdCfg.kexAlgorithms}
-      Ciphers ${concatStringsSep "," sshdCfg.ciphers}
-      MACs ${concatStringsSep "," sshdCfg.macs}
+      KexAlgorithms ${concatStringsSep "," sshdCfg.settings.KexAlgorithms}
+      Ciphers ${concatStringsSep "," sshdCfg.settings.Ciphers}
+      MACs ${concatStringsSep "," sshdCfg.settings.Macs}
 
-      LogLevel ${sshdCfg.logLevel}
+      LogLevel ${sshdCfg.settings.LogLevel}
 
-      ${if sshdCfg.useDns then ''
+      ${if sshdCfg.settings.UseDns then ''
         UseDNS yes
       '' else ''
         UseDNS no
@@ -142,7 +149,7 @@ in
 
       ${cfg.extraConfig}
     '';
-  in mkIf (config.boot.initrd.network.enable && cfg.enable) {
+  in mkIf enabled {
     assertions = [
       {
         assertion = cfg.authorizedKeys != [];
@@ -157,14 +164,19 @@ in
           for instructions.
         '';
       }
+
+      {
+        assertion = config.boot.initrd.systemd.enable -> cfg.shell == null;
+        message = "systemd stage 1 does not support boot.initrd.network.ssh.shell";
+      }
     ];
 
-    boot.initrd.extraUtilsCommands = ''
-      copy_bin_and_libs ${pkgs.openssh}/bin/sshd
+    boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
+      copy_bin_and_libs ${package}/bin/sshd
       cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib
     '';
 
-    boot.initrd.extraUtilsCommandsTest = ''
+    boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
       # sshd requires a host key to check config, so we pass in the test's
       tmpkey="$(mktemp initrd-ssh-testkey.XXXXXXXXXX)"
       cp "${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}" "$tmpkey"
@@ -176,9 +188,9 @@ in
       rm "$tmpkey"
     '';
 
-    boot.initrd.network.postCommands = ''
-      echo '${cfg.shell}' > /etc/shells
-      echo 'root:x:0:0:root:/root:${cfg.shell}' > /etc/passwd
+    boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) ''
+      echo '${shell}' > /etc/shells
+      echo 'root:x:0:0:root:/root:${shell}' > /etc/passwd
       echo 'sshd:x:1:1:sshd:/var/empty:/bin/nologin' >> /etc/passwd
       echo 'passwd: files' > /etc/nsswitch.conf
 
@@ -204,7 +216,7 @@ in
       /bin/sshd -e
     '';
 
-    boot.initrd.postMountCommands = ''
+    boot.initrd.postMountCommands = mkIf (!config.boot.initrd.systemd.enable) ''
       # Stop sshd cleanly before stage 2.
       #
       # If you want to keep it around to debug post-mount SSH issues,
@@ -217,6 +229,38 @@ in
 
     boot.initrd.secrets = listToAttrs
       (map (path: nameValuePair (initrdKeyPath path) path) cfg.hostKeys);
+
+    # Systemd initrd stuff
+    boot.initrd.systemd = mkIf config.boot.initrd.systemd.enable {
+      users.sshd = { uid = 1; group = "sshd"; };
+      groups.sshd = { gid = 1; };
+
+      contents."/etc/ssh/authorized_keys.d/root".text =
+        concatStringsSep "\n" config.boot.initrd.network.ssh.authorizedKeys;
+      contents."/etc/ssh/sshd_config".text = sshdConfig;
+      storePaths = ["${package}/bin/sshd"];
+
+      services.sshd = {
+        description = "SSH Daemon";
+        wantedBy = ["initrd.target"];
+        after = ["network.target" "initrd-nixos-copy-secrets.service"];
+
+        # Keys from Nix store are world-readable, which sshd doesn't
+        # like. If this were a real nix store and not the initrd, we
+        # neither would nor could do this
+        preStart = flip concatMapStrings cfg.hostKeys (path: ''
+          /bin/chmod 0600 "${initrdKeyPath path}"
+        '');
+        unitConfig.DefaultDependencies = false;
+        serviceConfig = {
+          ExecStart = "${package}/bin/sshd -D -f /etc/ssh/sshd_config";
+          Type = "simple";
+          KillMode = "process";
+          Restart = "on-failure";
+        };
+      };
+    };
+
   };
 
 }
diff --git a/nixos/modules/system/boot/kernel.nix b/nixos/modules/system/boot/kernel.nix
index b13e50cb17d..0298e28f328 100644
--- a/nixos/modules/system/boot/kernel.nix
+++ b/nixos/modules/system/boot/kernel.nix
@@ -20,7 +20,7 @@ in
   ###### interface
 
   options = {
-    boot.kernel.enable = mkEnableOption (lib.mdDoc "the Linux kernel. This is useful for systemd-like containers which do not require a kernel.") // {
+    boot.kernel.enable = mkEnableOption (lib.mdDoc "the Linux kernel. This is useful for systemd-like containers which do not require a kernel") // {
       default = true;
     };
 
@@ -73,8 +73,46 @@ in
     boot.kernelPatches = mkOption {
       type = types.listOf types.attrs;
       default = [];
-      example = literalExpression "[ pkgs.kernelPatches.ubuntu_fan_4_4 ]";
-      description = lib.mdDoc "A list of additional patches to apply to the kernel.";
+      example = literalExpression ''
+        [
+          {
+            name = "foo";
+            patch = ./foo.patch;
+            extraStructuredConfig.FOO = lib.kernel.yes;
+            features.foo = true;
+          }
+        ]
+      '';
+      description = lib.mdDoc ''
+        A list of additional patches to apply to the kernel.
+
+        Every item should be an attribute set with the following attributes:
+
+        ```nix
+        {
+          name = "foo";                 # descriptive name, required
+
+          patch = ./foo.patch;          # path or derivation that contains the patch source
+                                        # (required, but can be null if only config changes
+                                        # are needed)
+
+          extraStructuredConfig = {     # attrset of extra configuration parameters
+            FOO = lib.kernel.yes;       # (without the CONFIG_ prefix, optional)
+          };                            # values should generally be lib.kernel.yes,
+                                        # lib.kernel.no or lib.kernel.module
+
+          features = {                  # attrset of extra "features" the kernel is considered to have
+            foo = true;                 # (may be checked by other NixOS modules, optional)
+          };
+
+          extraConfig = "CONFIG_FOO y"; # extra configuration options in string form
+                                        # (deprecated, use extraStructuredConfig instead, optional)
+        }
+        ```
+
+        There's a small set of existing kernel patches in Nixpkgs, available as `pkgs.kernelPatches`,
+        that follow this format and can be used directly.
+      '';
     };
 
     boot.kernel.randstructSeed = mkOption {
diff --git a/nixos/modules/system/boot/loader/external/external.md b/nixos/modules/system/boot/loader/external/external.md
index ba1dfd4d9b9..4f5b559dfc4 100644
--- a/nixos/modules/system/boot/loader/external/external.md
+++ b/nixos/modules/system/boot/loader/external/external.md
@@ -20,7 +20,7 @@ You can enable FooBoot like this:
 }
 ```
 
-## Developing Custom Bootloader Backends
+## Developing Custom Bootloader Backends {#sec-bootloader-external-developing}
 
 Bootloaders should use [RFC-0125](https://github.com/NixOS/rfcs/pull/125)'s Bootspec format and synthesis tools to identify the key properties for bootable system generations.
 
diff --git a/nixos/modules/system/boot/loader/external/external.nix b/nixos/modules/system/boot/loader/external/external.nix
index 5cf478e6c83..926cbd2b4b3 100644
--- a/nixos/modules/system/boot/loader/external/external.nix
+++ b/nixos/modules/system/boot/loader/external/external.nix
@@ -8,9 +8,7 @@ in
 {
   meta = {
     maintainers = with maintainers; [ cole-h grahamc raitobezarius ];
-    # Don't edit the docbook xml directly, edit the md and generate it:
-    # `pandoc external.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > external.xml`
-    doc = ./external.xml;
+    doc = ./external.md;
   };
 
   options.boot.loader.external = {
diff --git a/nixos/modules/system/boot/loader/external/external.xml b/nixos/modules/system/boot/loader/external/external.xml
deleted file mode 100644
index 39ab2156bc8..00000000000
--- a/nixos/modules/system/boot/loader/external/external.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-bootloader-external">
-  <title>External Bootloader Backends</title>
-  <para>
-    NixOS has support for several bootloader backends by default:
-    systemd-boot, grub, uboot, etc. The built-in bootloader backend
-    support is generic and supports most use cases. Some users may
-    prefer to create advanced workflows around managing the bootloader
-    and bootable entries.
-  </para>
-  <para>
-    You can replace the built-in bootloader support with your own
-    tooling using the <quote>external</quote> bootloader option.
-  </para>
-  <para>
-    Imagine you have created a new package called FooBoot. FooBoot
-    provides a program at
-    <literal>${pkgs.fooboot}/bin/fooboot-install</literal> which takes
-    the system closure’s path as its only argument and configures the
-    system’s bootloader.
-  </para>
-  <para>
-    You can enable FooBoot like this:
-  </para>
-  <programlisting language="nix">
-{ pkgs, ... }: {
-  boot.loader.external = {
-    enable = true;
-    installHook = &quot;${pkgs.fooboot}/bin/fooboot-install&quot;;
-  };
-}
-</programlisting>
-  <section xml:id="developing-custom-bootloader-backends">
-    <title>Developing Custom Bootloader Backends</title>
-    <para>
-      Bootloaders should use
-      <link xlink:href="https://github.com/NixOS/rfcs/pull/125">RFC-0125</link>’s
-      Bootspec format and synthesis tools to identify the key properties
-      for bootable system generations.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix
index 1d266b5a37d..b57e343d2ac 100644
--- a/nixos/modules/system/boot/loader/grub/grub.nix
+++ b/nixos/modules/system/boot/loader/grub/grub.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, options, lib, pkgs, ... }:
 
 with lib;
 
@@ -12,13 +12,8 @@ let
     # Package set of targeted architecture
     if cfg.forcei686 then pkgs.pkgsi686Linux else pkgs;
 
-  realGrub = if cfg.version == 1 then grubPkgs.grub
-    else if cfg.zfsSupport then grubPkgs.grub2.override { zfsSupport = true; }
-    else if cfg.trustedBoot.enable
-         then if cfg.trustedBoot.isHPLaptop
-              then grubPkgs.trustedGrub-for-HP
-              else grubPkgs.trustedGrub
-         else grubPkgs.grub2;
+  realGrub = if cfg.zfsSupport then grubPkgs.grub2.override { zfsSupport = true; }
+    else grubPkgs.grub2;
 
   grub =
     # Don't include GRUB if we're only generating a GRUB menu (e.g.,
@@ -28,12 +23,11 @@ let
     else realGrub;
 
   grubEfi =
-    # EFI version of Grub v2
-    if cfg.efiSupport && (cfg.version == 2)
+    if cfg.efiSupport
     then realGrub.override { efiSupport = cfg.efiSupport; }
     else null;
 
-  f = x: if x == null then "" else "" + x;
+  f = x: optionalString (x != null) ("" + x);
 
   grubConfig = args:
     let
@@ -52,24 +46,24 @@ let
       fullName = lib.getName realGrub;
       fullVersion = lib.getVersion realGrub;
       grubEfi = f grubEfi;
-      grubTargetEfi = if cfg.efiSupport && (cfg.version == 2) then f (grubEfi.grubTarget or "") else "";
+      grubTargetEfi = optionalString cfg.efiSupport (f (grubEfi.grubTarget or ""));
       bootPath = args.path;
       storePath = config.boot.loader.grub.storePath;
-      bootloaderId = if args.efiBootloaderId == null then "NixOS${efiSysMountPoint'}" else args.efiBootloaderId;
+      bootloaderId = if args.efiBootloaderId == null then "${config.system.nixos.distroName}${efiSysMountPoint'}" else args.efiBootloaderId;
       timeout = if config.boot.loader.timeout == null then -1 else config.boot.loader.timeout;
-      users = if cfg.users == {} || cfg.version != 1 then cfg.users else throw "GRUB version 1 does not support user accounts.";
       theme = f cfg.theme;
       inherit efiSysMountPoint;
       inherit (args) devices;
       inherit (efi) canTouchEfiVariables;
       inherit (cfg)
-        version extraConfig extraPerEntryConfig extraEntries forceInstall useOSProber
+        extraConfig extraPerEntryConfig extraEntries forceInstall useOSProber
         extraGrubInstallArgs
         extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels
-        default fsIdentifier efiSupport efiInstallAsRemovable gfxmodeEfi gfxmodeBios gfxpayloadEfi gfxpayloadBios;
+        default fsIdentifier efiSupport efiInstallAsRemovable gfxmodeEfi gfxmodeBios gfxpayloadEfi gfxpayloadBios
+        users;
       path = with pkgs; makeBinPath (
         [ coreutils gnused gnugrep findutils diffutils btrfs-progs util-linux mdadm ]
-        ++ optional (cfg.efiSupport && (cfg.version == 2)) efibootmgr
+        ++ optional cfg.efiSupport efibootmgr
         ++ optionals cfg.useOSProber [ busybox os-prober ]);
       font = if cfg.font == null then ""
         else (if lib.last (lib.splitString "." cfg.font) == "pf2"
@@ -109,14 +103,8 @@ in
       };
 
       version = mkOption {
-        default = 2;
-        example = 1;
+        visible = false;
         type = types.int;
-        description = lib.mdDoc ''
-          The version of GRUB to use: `1` for GRUB
-          Legacy (versions 0.9x), or `2` (the
-          default) for GRUB 2.
-        '';
       };
 
       device = mkOption {
@@ -364,10 +352,6 @@ in
         default = "";
         type = types.lines;
         example = ''
-          # GRUB 1 example (not GRUB 2 compatible)
-          title Windows
-            chainloader (hd0,1)+1
-
           # GRUB 2 example
           menuentry "Windows 7" {
             chainloader (hd0,4)+1
@@ -422,14 +406,6 @@ in
           Set to `null` to run GRUB in text mode.
 
           ::: {.note}
-          For grub 1:
-          It must be a 640x480,
-          14-colour image in XPM format, optionally compressed with
-          {command}`gzip` or {command}`bzip2`.
-          :::
-
-          ::: {.note}
-          For grub 2:
           File must be one of .png, .tga, .jpg, or .jpeg. JPEG images must
           not be progressive.
           The image will be scaled if necessary to fit the screen.
@@ -443,10 +419,6 @@ in
         default = null;
         description = lib.mdDoc ''
           Background color to be used for GRUB to fill the areas the image isn't filling.
-
-          ::: {.note}
-          This options has no effect for GRUB 1.
-          :::
         '';
       };
 
@@ -455,10 +427,6 @@ in
         type = types.nullOr types.str;
         description = lib.mdDoc ''
           Options applied to the primary NixOS menu entry.
-
-          ::: {.note}
-          This options has no effect for GRUB 1.
-          :::
         '';
       };
 
@@ -467,10 +435,6 @@ in
         type = types.nullOr types.str;
         description = lib.mdDoc ''
           Options applied to the secondary NixOS submenu entry.
-
-          ::: {.note}
-          This options has no effect for GRUB 1.
-          :::
         '';
       };
 
@@ -480,10 +444,6 @@ in
         default = null;
         description = lib.mdDoc ''
           Grub theme to be used.
-
-          ::: {.note}
-          This options has no effect for GRUB 1.
-          :::
         '';
       };
 
@@ -492,10 +452,6 @@ in
         default = "stretch";
         description = lib.mdDoc ''
           Whether to stretch the image or show the image in the top-left corner unstretched.
-
-          ::: {.note}
-          This options has no effect for GRUB 1.
-          :::
         '';
       };
 
@@ -604,8 +560,6 @@ in
         type = types.bool;
         description = lib.mdDoc ''
           Whether GRUB should be built against libzfs.
-          ZFS support is only available for GRUB v2.
-          This option is ignored for GRUB v1.
         '';
       };
 
@@ -614,8 +568,6 @@ in
         type = types.bool;
         description = lib.mdDoc ''
           Whether GRUB should be built with EFI support.
-          EFI support is only available for GRUB v2.
-          This option is ignored for GRUB v1.
         '';
       };
 
@@ -682,39 +634,6 @@ in
         '';
       };
 
-      trustedBoot = {
-
-        enable = mkOption {
-          default = false;
-          type = types.bool;
-          description = lib.mdDoc ''
-            Enable trusted boot. GRUB will measure all critical components during
-            the boot process to offer TCG (TPM) support.
-          '';
-        };
-
-        systemHasTPM = mkOption {
-          default = "";
-          example = "YES_TPM_is_activated";
-          type = types.str;
-          description = lib.mdDoc ''
-            Assertion that the target system has an activated TPM. It is a safety
-            check before allowing the activation of 'trustedBoot.enable'. TrustedBoot
-            WILL FAIL TO BOOT YOUR SYSTEM if no TPM is available.
-          '';
-        };
-
-        isHPLaptop = mkOption {
-          default = false;
-          type = types.bool;
-          description = lib.mdDoc ''
-            Use a special version of TrustedGRUB that is needed by some HP laptops
-            and works only for the HP laptops.
-          '';
-        };
-
-      };
-
     };
 
   };
@@ -724,14 +643,7 @@ in
 
   config = mkMerge [
 
-    { boot.loader.grub.splashImage = mkDefault (
-        if cfg.version == 1 then pkgs.fetchurl {
-          url = "http://www.gnome-look.org/CONTENT/content-files/36909-soft-tux.xpm.gz";
-          sha256 = "14kqdx2lfqvh40h6fjjzqgff1mwk74dmbjvmqphi6azzra7z8d59";
-        }
-        # GRUB 1.97 doesn't support gzipped XPMs.
-        else defaultSplash);
-    }
+    { boot.loader.grub.splashImage = mkDefault defaultSplash; }
 
     (mkIf (cfg.splashImage == defaultSplash) {
       boot.loader.grub.backgroundColor = mkDefault "#2F302F";
@@ -759,6 +671,7 @@ in
             src = ./install-grub.pl;
             utillinux = pkgs.util-linux;
             btrfsprogs = pkgs.btrfs-progs;
+            inherit (config.system.nixos) distroName;
           };
           perl = pkgs.perl.withPackages (p: with p; [
             FileSlurp FileCopyRecursive
@@ -788,10 +701,6 @@ in
 
       assertions = [
         {
-          assertion = !cfg.zfsSupport || cfg.version == 2;
-          message = "Only GRUB version 2 provides ZFS support";
-        }
-        {
           assertion = cfg.mirroredBoots != [ ];
           message = "You must set the option ‘boot.loader.grub.devices’ or "
             + "'boot.loader.grub.mirroredBoots' to make the system bootable.";
@@ -801,22 +710,6 @@ in
           message = "You cannot have duplicated devices in mirroredBoots";
         }
         {
-          assertion = !cfg.trustedBoot.enable || cfg.version == 2;
-          message = "Trusted GRUB is only available for GRUB 2";
-        }
-        {
-          assertion = !cfg.efiSupport || !cfg.trustedBoot.enable;
-          message = "Trusted GRUB does not have EFI support";
-        }
-        {
-          assertion = !cfg.zfsSupport || !cfg.trustedBoot.enable;
-          message = "Trusted GRUB does not have ZFS support";
-        }
-        {
-          assertion = !cfg.trustedBoot.enable || cfg.trustedBoot.systemHasTPM == "YES_TPM_is_activated";
-          message = "Trusted GRUB can break the system! Confirm that the system has an activated TPM by setting 'systemHasTPM'.";
-        }
-        {
           assertion = cfg.efiInstallAsRemovable -> cfg.efiSupport;
           message = "If you wish to to use boot.loader.grub.efiInstallAsRemovable, then turn on boot.loader.grub.efiSupport";
         }
@@ -824,6 +717,10 @@ in
           assertion = cfg.efiInstallAsRemovable -> !config.boot.loader.efi.canTouchEfiVariables;
           message = "If you wish to to use boot.loader.grub.efiInstallAsRemovable, then turn off boot.loader.efi.canTouchEfiVariables";
         }
+        {
+          assertion = !(options.boot.loader.grub.version.isDefined && cfg.version == 1);
+          message = "Support for version 0.9x of GRUB was removed after being unsupported upstream for around a decade";
+        }
       ] ++ flip concatMap cfg.mirroredBoots (args: [
         {
           assertion = args.devices != [ ];
@@ -843,6 +740,11 @@ in
       }));
     })
 
+    (mkIf options.boot.loader.grub.version.isDefined {
+      warnings = [ ''
+        The boot.loader.grub.version option does not have any effect anymore, please remove it from your configuration.
+      '' ];
+    })
   ];
 
 
@@ -854,6 +756,10 @@ in
       (mkRenamedOptionModule [ "boot" "grubDevice" ] [ "boot" "loader" "grub" "device" ])
       (mkRenamedOptionModule [ "boot" "bootMount" ] [ "boot" "loader" "grub" "bootDevice" ])
       (mkRenamedOptionModule [ "boot" "grubSplashImage" ] [ "boot" "loader" "grub" "splashImage" ])
+      (mkRemovedOptionModule [ "boot" "loader" "grub" "trustedBoot" ] ''
+        Support for Trusted GRUB has been removed, because the project
+        has been retired upstream.
+      '')
       (mkRemovedOptionModule [ "boot" "loader" "grub" "extraInitrd" ] ''
         This option has been replaced with the bootloader agnostic
         boot.initrd.secrets option. To migrate to the initrd secrets system,
diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl
index d5f019423b6..27f03f2fb58 100644
--- a/nixos/modules/system/boot/loader/grub/install-grub.pl
+++ b/nixos/modules/system/boot/loader/grub/install-grub.pl
@@ -34,28 +34,33 @@ sub getList {
 }
 
 sub readFile {
-    my ($fn) = @_; local $/ = undef;
-    open FILE, "<$fn" or return undef; my $s = <FILE>; close FILE;
-    local $/ = "\n"; chomp $s; return $s;
+    my ($fn) = @_;
+    # enable slurp mode: read entire file in one go
+    local $/ = undef;
+    open my $fh, "<$fn" or return undef;
+    my $s = <$fh>;
+    close $fh;
+    # disable slurp mode
+    local $/ = "\n";
+    chomp $s;
+    return $s;
 }
 
 sub writeFile {
     my ($fn, $s) = @_;
-    open FILE, ">$fn" or die "cannot create $fn: $!\n";
-    print FILE $s or die;
-    close FILE or die;
+    open my $fh, ">$fn" or die "cannot create $fn: $!\n";
+    print $fh $s or die "cannot write to $fn: $!\n";
+    close $fh or die "cannot close $fn: $!\n";
 }
 
 sub runCommand {
-    my ($cmd) = @_;
-    open FILE, "$cmd 2>/dev/null |" or die "Failed to execute: $cmd\n";
-    my @ret = <FILE>;
-    close FILE;
+    open(my $fh, "-|", @_) or die "Failed to execute: $@_\n";
+    my @ret = $fh->getlines();
+    close $fh;
     return ($?, @ret);
 }
 
 my $grub = get("grub");
-my $grubVersion = int(get("version"));
 my $grubTarget = get("grubTarget");
 my $extraConfig = get("extraConfig");
 my $extraPrepareConfig = get("extraPrepareConfig");
@@ -90,9 +95,7 @@ my $theme = get("theme");
 my $saveDefault = $defaultEntry eq "saved";
 $ENV{'PATH'} = get("path");
 
-die "unsupported GRUB version\n" if $grubVersion != 1 && $grubVersion != 2;
-
-print STDERR "updating GRUB $grubVersion menu...\n";
+print STDERR "updating GRUB 2 menu...\n";
 
 mkpath("$bootPath/grub", 0, 0700);
 
@@ -170,76 +173,74 @@ sub GrubFs {
     }
     my $search = "";
 
-    if ($grubVersion > 1) {
-        # ZFS is completely separate logic as zpools are always identified by a label
-        # or custom UUID
-        if ($fs->type eq 'zfs') {
-            my $sid = index($fs->device, '/');
-
-            if ($sid < 0) {
-                $search = '--label ' . $fs->device;
-                $path = '/@' . $path;
-            } else {
-                $search = '--label ' . substr($fs->device, 0, $sid);
-                $path = '/' . substr($fs->device, $sid) . '/@' . $path;
+    # ZFS is completely separate logic as zpools are always identified by a label
+    # or custom UUID
+    if ($fs->type eq 'zfs') {
+        my $sid = index($fs->device, '/');
+
+        if ($sid < 0) {
+            $search = '--label ' . $fs->device;
+            $path = '/@' . $path;
+        } else {
+            $search = '--label ' . substr($fs->device, 0, $sid);
+            $path = '/' . substr($fs->device, $sid) . '/@' . $path;
+        }
+    } else {
+        my %types = ('uuid' => '--fs-uuid', 'label' => '--label');
+
+        if ($fsIdentifier eq 'provided') {
+            # If the provided dev is identifying the partition using a label or uuid,
+            # we should get the label / uuid and do a proper search
+            my @matches = $fs->device =~ m/\/dev\/disk\/by-(label|uuid)\/(.*)/;
+            if ($#matches > 1) {
+                die "Too many matched devices"
+            } elsif ($#matches == 1) {
+                $search = "$types{$matches[0]} $matches[1]"
             }
         } else {
-            my %types = ('uuid' => '--fs-uuid', 'label' => '--label');
-
-            if ($fsIdentifier eq 'provided') {
-                # If the provided dev is identifying the partition using a label or uuid,
-                # we should get the label / uuid and do a proper search
-                my @matches = $fs->device =~ m/\/dev\/disk\/by-(label|uuid)\/(.*)/;
-                if ($#matches > 1) {
-                    die "Too many matched devices"
-                } elsif ($#matches == 1) {
-                    $search = "$types{$matches[0]} $matches[1]"
-                }
-            } else {
-                # Determine the identifying type
-                $search = $types{$fsIdentifier} . ' ';
+            # Determine the identifying type
+            $search = $types{$fsIdentifier} . ' ';
 
-                # Based on the type pull in the identifier from the system
-                my ($status, @devInfo) = runCommand("@utillinux@/bin/blkid -o export @{[$fs->device]}");
-                if ($status != 0) {
-                    die "Failed to get blkid info (returned $status) for @{[$fs->mount]} on @{[$fs->device]}";
-                }
-                my @matches = join("", @devInfo) =~ m/@{[uc $fsIdentifier]}=([^\n]*)/;
-                if ($#matches != 0) {
-                    die "Couldn't find a $types{$fsIdentifier} for @{[$fs->device]}\n"
-                }
-                $search .= $matches[0];
+            # Based on the type pull in the identifier from the system
+            my ($status, @devInfo) = runCommand("@utillinux@/bin/blkid", "-o", "export", @{[$fs->device]});
+            if ($status != 0) {
+                die "Failed to get blkid info (returned $status) for @{[$fs->mount]} on @{[$fs->device]}";
+            }
+            my @matches = join("", @devInfo) =~ m/@{[uc $fsIdentifier]}=([^\n]*)/;
+            if ($#matches != 0) {
+                die "Couldn't find a $types{$fsIdentifier} for @{[$fs->device]}\n"
             }
+            $search .= $matches[0];
+        }
 
-            # BTRFS is a special case in that we need to fix the referrenced path based on subvolumes
-            if ($fs->type eq 'btrfs') {
-                my ($status, @id_info) = runCommand("@btrfsprogs@/bin/btrfs subvol show @{[$fs->mount]}");
+        # BTRFS is a special case in that we need to fix the referenced path based on subvolumes
+        if ($fs->type eq 'btrfs') {
+            my ($status, @id_info) = runCommand("@btrfsprogs@/bin/btrfs", "subvol", "show", @{[$fs->mount]});
+            if ($status != 0) {
+                die "Failed to retrieve subvolume info for @{[$fs->mount]}\n";
+            }
+            my @ids = join("\n", @id_info) =~ m/^(?!\/\n).*Subvolume ID:[ \t\n]*([0-9]+)/s;
+            if ($#ids > 0) {
+                die "Btrfs subvol name for @{[$fs->device]} listed multiple times in mount\n"
+            } elsif ($#ids == 0) {
+                my ($status, @path_info) = runCommand("@btrfsprogs@/bin/btrfs", "subvol", "list", @{[$fs->mount]});
                 if ($status != 0) {
-                    die "Failed to retrieve subvolume info for @{[$fs->mount]}\n";
+                    die "Failed to find @{[$fs->mount]} subvolume id from btrfs\n";
                 }
-                my @ids = join("\n", @id_info) =~ m/^(?!\/\n).*Subvolume ID:[ \t\n]*([0-9]+)/s;
-                if ($#ids > 0) {
-                    die "Btrfs subvol name for @{[$fs->device]} listed multiple times in mount\n"
-                } elsif ($#ids == 0) {
-                    my ($status, @path_info) = runCommand("@btrfsprogs@/bin/btrfs subvol list @{[$fs->mount]}");
-                    if ($status != 0) {
-                        die "Failed to find @{[$fs->mount]} subvolume id from btrfs\n";
-                    }
-                    my @paths = join("", @path_info) =~ m/ID $ids[0] [^\n]* path ([^\n]*)/;
-                    if ($#paths > 0) {
-                        die "Btrfs returned multiple paths for a single subvolume id, mountpoint @{[$fs->mount]}\n";
-                    } elsif ($#paths != 0) {
-                        die "Btrfs did not return a path for the subvolume at @{[$fs->mount]}\n";
-                    }
-                    $path = "/$paths[0]$path";
+                my @paths = join("", @path_info) =~ m/ID $ids[0] [^\n]* path ([^\n]*)/;
+                if ($#paths > 0) {
+                    die "Btrfs returned multiple paths for a single subvolume id, mountpoint @{[$fs->mount]}\n";
+                } elsif ($#paths != 0) {
+                    die "Btrfs did not return a path for the subvolume at @{[$fs->mount]}\n";
                 }
+                $path = "/$paths[0]$path";
             }
         }
-        if (not $search eq "") {
-            $search = "search --set=drive$driveid " . $search;
-            $path = "(\$drive$driveid)$path";
-            $driveid += 1;
-        }
+    }
+    if (not $search eq "") {
+        $search = "search --set=drive$driveid " . $search;
+        $path = "(\$drive$driveid)$path";
+        $driveid += 1;
     }
     return Grub->new(path => $path, search => $search);
 }
@@ -252,166 +253,151 @@ if ($copyKernels == 0) {
 # Generate the header.
 my $conf .= "# Automatically generated.  DO NOT EDIT THIS FILE!\n";
 
-if ($grubVersion == 1) {
-    # $defaultEntry might be "saved", indicating that we want to use the last selected configuration as default.
-    # Incidentally this is already the correct value for the grub 1 config to achieve this behaviour.
-    $conf .= "
-        default $defaultEntry
-        timeout $timeout
-    ";
-    if ($splashImage) {
-        copy $splashImage, "$bootPath/background.xpm.gz" or die "cannot copy $splashImage to $bootPath: $!\n";
-        $conf .= "splashimage " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background.xpm.gz\n";
+my @users = ();
+foreach my $user ($dom->findnodes('/expr/attrs/attr[@name = "users"]/attrs/attr')) {
+    my $name = $user->findvalue('@name') or die;
+    my $hashedPassword = $user->findvalue('./attrs/attr[@name = "hashedPassword"]/string/@value');
+    my $hashedPasswordFile = $user->findvalue('./attrs/attr[@name = "hashedPasswordFile"]/string/@value');
+    my $password = $user->findvalue('./attrs/attr[@name = "password"]/string/@value');
+    my $passwordFile = $user->findvalue('./attrs/attr[@name = "passwordFile"]/string/@value');
+
+    if ($hashedPasswordFile) {
+        open(my $f, '<', $hashedPasswordFile) or die "Can't read file '$hashedPasswordFile'!";
+        $hashedPassword = <$f>;
+        chomp $hashedPassword;
+    }
+    if ($passwordFile) {
+        open(my $f, '<', $passwordFile) or die "Can't read file '$passwordFile'!";
+        $password = <$f>;
+        chomp $password;
     }
-}
-
-else {
-    my @users = ();
-    foreach my $user ($dom->findnodes('/expr/attrs/attr[@name = "users"]/attrs/attr')) {
-        my $name = $user->findvalue('@name') or die;
-        my $hashedPassword = $user->findvalue('./attrs/attr[@name = "hashedPassword"]/string/@value');
-        my $hashedPasswordFile = $user->findvalue('./attrs/attr[@name = "hashedPasswordFile"]/string/@value');
-        my $password = $user->findvalue('./attrs/attr[@name = "password"]/string/@value');
-        my $passwordFile = $user->findvalue('./attrs/attr[@name = "passwordFile"]/string/@value');
-
-        if ($hashedPasswordFile) {
-            open(my $f, '<', $hashedPasswordFile) or die "Can't read file '$hashedPasswordFile'!";
-            $hashedPassword = <$f>;
-            chomp $hashedPassword;
-        }
-        if ($passwordFile) {
-            open(my $f, '<', $passwordFile) or die "Can't read file '$passwordFile'!";
-            $password = <$f>;
-            chomp $password;
-        }
 
-        if ($hashedPassword) {
-            if (index($hashedPassword, "grub.pbkdf2.") == 0) {
-                $conf .= "\npassword_pbkdf2 $name $hashedPassword";
-            }
-            else {
-                die "Password hash for GRUB user '$name' is not valid!";
-            }
-        }
-        elsif ($password) {
-            $conf .= "\npassword $name $password";
+    if ($hashedPassword) {
+        if (index($hashedPassword, "grub.pbkdf2.") == 0) {
+            $conf .= "\npassword_pbkdf2 $name $hashedPassword";
         }
         else {
-            die "GRUB user '$name' has no password!";
+            die "Password hash for GRUB user '$name' is not valid!";
         }
-        push(@users, $name);
     }
-    if (@users) {
-        $conf .= "\nset superusers=\"" . join(' ',@users) . "\"\n";
+    elsif ($password) {
+        $conf .= "\npassword $name $password";
     }
-
-    if ($copyKernels == 0) {
-        $conf .= "
-            " . $grubStore->search;
+    else {
+        die "GRUB user '$name' has no password!";
     }
-    # FIXME: should use grub-mkconfig.
-    my $defaultEntryText = $defaultEntry;
-    if ($saveDefault) {
-        $defaultEntryText = "\"\${saved_entry}\"";
-    }
-    $conf .= "
-        " . $grubBoot->search . "
-        if [ -s \$prefix/grubenv ]; then
-          load_env
-        fi
+    push(@users, $name);
+}
+if (@users) {
+    $conf .= "\nset superusers=\"" . join(' ',@users) . "\"\n";
+}
 
-        # ‘grub-reboot’ sets a one-time saved entry, which we process here and
-        # then delete.
-        if [ \"\${next_entry}\" ]; then
-          set default=\"\${next_entry}\"
-          set next_entry=
-          save_env next_entry
-          set timeout=1
-          set boot_once=true
-        else
-          set default=$defaultEntryText
-          set timeout=$timeout
+if ($copyKernels == 0) {
+    $conf .= "
+        " . $grubStore->search;
+}
+# FIXME: should use grub-mkconfig.
+my $defaultEntryText = $defaultEntry;
+if ($saveDefault) {
+    $defaultEntryText = "\"\${saved_entry}\"";
+}
+$conf .= "
+    " . $grubBoot->search . "
+    if [ -s \$prefix/grubenv ]; then
+      load_env
+    fi
+
+    # ‘grub-reboot’ sets a one-time saved entry, which we process here and
+    # then delete.
+    if [ \"\${next_entry}\" ]; then
+      set default=\"\${next_entry}\"
+      set next_entry=
+      save_env next_entry
+      set timeout=1
+      set boot_once=true
+    else
+      set default=$defaultEntryText
+      set timeout=$timeout
+    fi
+
+    function savedefault {
+        if [ -z \"\${boot_once}\"]; then
+        saved_entry=\"\${chosen}\"
+        save_env saved_entry
         fi
+    }
 
-        function savedefault {
-            if [ -z \"\${boot_once}\"]; then
-            saved_entry=\"\${chosen}\"
-            save_env saved_entry
-            fi
-        }
-
-        # Setup the graphics stack for bios and efi systems
-        if [ \"\${grub_platform}\" = \"efi\" ]; then
-          insmod efi_gop
-          insmod efi_uga
-        else
-          insmod vbe
+    # Setup the graphics stack for bios and efi systems
+    if [ \"\${grub_platform}\" = \"efi\" ]; then
+      insmod efi_gop
+      insmod efi_uga
+    else
+      insmod vbe
+    fi
+";
+
+if ($font) {
+    copy $font, "$bootPath/converted-font.pf2" or die "cannot copy $font to $bootPath: $!\n";
+    $conf .= "
+        insmod font
+        if loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/converted-font.pf2; then
+          insmod gfxterm
+          if [ \"\${grub_platform}\" = \"efi\" ]; then
+            set gfxmode=$gfxmodeEfi
+            set gfxpayload=$gfxpayloadEfi
+          else
+            set gfxmode=$gfxmodeBios
+            set gfxpayload=$gfxpayloadBios
+          fi
+          terminal_output gfxterm
         fi
     ";
-
-    if ($font) {
-        copy $font, "$bootPath/converted-font.pf2" or die "cannot copy $font to $bootPath: $!\n";
-        $conf .= "
-            insmod font
-            if loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/converted-font.pf2; then
-              insmod gfxterm
-              if [ \"\${grub_platform}\" = \"efi\" ]; then
-                set gfxmode=$gfxmodeEfi
-                set gfxpayload=$gfxpayloadEfi
-              else
-                set gfxmode=$gfxmodeBios
-                set gfxpayload=$gfxpayloadBios
-              fi
-              terminal_output gfxterm
-            fi
-        ";
+}
+if ($splashImage) {
+    # Keeps the image's extension.
+    my ($filename, $dirs, $suffix) = fileparse($splashImage, qr"\..[^.]*$");
+    # The module for jpg is jpeg.
+    if ($suffix eq ".jpg") {
+        $suffix = ".jpeg";
     }
-    if ($splashImage) {
-        # Keeps the image's extension.
-        my ($filename, $dirs, $suffix) = fileparse($splashImage, qr"\..[^.]*$");
-        # The module for jpg is jpeg.
-        if ($suffix eq ".jpg") {
-            $suffix = ".jpeg";
-        }
-        if ($backgroundColor) {
-            $conf .= "
-            background_color '$backgroundColor'
-            ";
-        }
-        copy $splashImage, "$bootPath/background$suffix" or die "cannot copy $splashImage to $bootPath: $!\n";
+    if ($backgroundColor) {
         $conf .= "
-            insmod " . substr($suffix, 1) . "
-            if background_image --mode '$splashMode' " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background$suffix; then
-              set color_normal=white/black
-              set color_highlight=black/white
-            else
-              set menu_color_normal=cyan/blue
-              set menu_color_highlight=white/blue
-            fi
+        background_color '$backgroundColor'
         ";
     }
+    copy $splashImage, "$bootPath/background$suffix" or die "cannot copy $splashImage to $bootPath: $!\n";
+    $conf .= "
+        insmod " . substr($suffix, 1) . "
+        if background_image --mode '$splashMode' " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/background$suffix; then
+          set color_normal=white/black
+          set color_highlight=black/white
+        else
+          set menu_color_normal=cyan/blue
+          set menu_color_highlight=white/blue
+        fi
+    ";
+}
 
-    rmtree("$bootPath/theme") or die "cannot clean up theme folder in $bootPath\n" if -e "$bootPath/theme";
+rmtree("$bootPath/theme") or die "cannot clean up theme folder in $bootPath\n" if -e "$bootPath/theme";
 
-    if ($theme) {
-        # Copy theme
-        rcopy($theme, "$bootPath/theme") or die "cannot copy $theme to $bootPath\n";
-        $conf .= "
-            # Sets theme.
-            set theme=" . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/theme.txt
-            export theme
-            # Load theme fonts, if any
-        ";
+if ($theme) {
+    # Copy theme
+    rcopy($theme, "$bootPath/theme") or die "cannot copy $theme to $bootPath\n";
+    $conf .= "
+        # Sets theme.
+        set theme=" . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/theme.txt
+        export theme
+        # Load theme fonts, if any
+    ";
 
-        find( { wanted => sub {
-            if ($_ =~ /\.pf2$/i) {
-                $font = File::Spec->abs2rel($File::Find::name, $theme);
-                $conf .= "
-                    loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/$font
-                ";
-            }
-        }, no_chdir => 1 }, $theme );
-    }
+    find( { wanted => sub {
+        if ($_ =~ /\.pf2$/i) {
+            $font = File::Spec->abs2rel($File::Find::name, $theme);
+            $conf .= "
+                loadfont " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/theme/$font
+            ";
+        }
+    }, no_chdir => 1 }, $theme );
 }
 
 $conf .= "$extraConfig\n";
@@ -442,7 +428,7 @@ sub copyToKernelsDir {
 }
 
 sub addEntry {
-    my ($name, $path, $options) = @_;
+    my ($name, $path, $options, $current) = @_;
     return unless -e "$path/kernel" && -e "$path/initrd";
 
     my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel"));
@@ -450,20 +436,28 @@ sub addEntry {
 
     # Include second initrd with secrets
     if (-e -x "$path/append-initrd-secrets") {
-        my $initrdName = basename($initrd);
-        my $initrdSecretsPath = "$bootPath/kernels/$initrdName-secrets";
+        # Name the initrd secrets after the system from which they're derived.
+        my $systemName = basename(Cwd::abs_path("$path"));
+        my $initrdSecretsPath = "$bootPath/kernels/$systemName-secrets";
 
         mkpath(dirname($initrdSecretsPath), 0, 0755);
         my $oldUmask = umask;
         # Make sure initrd is not world readable (won't work if /boot is FAT)
         umask 0137;
         my $initrdSecretsPathTemp = File::Temp::mktemp("$initrdSecretsPath.XXXXXXXX");
-        system("$path/append-initrd-secrets", $initrdSecretsPathTemp) == 0 or die "failed to create initrd secrets: $!\n";
+        if (system("$path/append-initrd-secrets", $initrdSecretsPathTemp) != 0) {
+          if ($current) {
+              die "failed to create initrd secrets $!\n";
+          } else {
+              say STDERR "warning: failed to create initrd secrets for \"$name\", an older generation";
+              say STDERR "note: this is normal after having removed or renamed a file in `boot.initrd.secrets`";
+          }
+        }
         # Check whether any secrets were actually added
         if (-e $initrdSecretsPathTemp && ! -z _) {
             rename $initrdSecretsPathTemp, $initrdSecretsPath or die "failed to move initrd secrets into place: $!\n";
             $copied{$initrdSecretsPath} = 1;
-            $initrd .= " " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$initrdName-secrets";
+            $initrd .= " " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$systemName-secrets";
         } else {
             unlink $initrdSecretsPathTemp;
             rmdir dirname($initrdSecretsPathTemp);
@@ -480,38 +474,26 @@ sub addEntry {
         readFile("$path/kernel-params");
     my $xenParams = $xen && -e "$path/xen-params" ? readFile("$path/xen-params") : "";
 
-    if ($grubVersion == 1) {
-        $conf .= "title $name\n";
-        $conf .= "  $extraPerEntryConfig\n" if $extraPerEntryConfig;
-        $conf .= "  kernel $xen $xenParams\n" if $xen;
-        $conf .= "  " . ($xen ? "module" : "kernel") . " $kernel $kernelParams\n";
-        $conf .= "  " . ($xen ? "module" : "initrd") . " $initrd\n";
-        if ($saveDefault) {
-            $conf .= "  savedefault\n";
-        }
-        $conf .= "\n";
-    } else {
-        $conf .= "menuentry \"$name\" " . ($options||"") . " {\n";
-        if ($saveDefault) {
-            $conf .= "  savedefault\n";
-        }
-        $conf .= $grubBoot->search . "\n";
-        if ($copyKernels == 0) {
-            $conf .= $grubStore->search . "\n";
-        }
-        $conf .= "  $extraPerEntryConfig\n" if $extraPerEntryConfig;
-        $conf .= "  multiboot $xen $xenParams\n" if $xen;
-        $conf .= "  " . ($xen ? "module" : "linux") . " $kernel $kernelParams\n";
-        $conf .= "  " . ($xen ? "module" : "initrd") . " $initrd\n";
-        $conf .= "}\n\n";
+    $conf .= "menuentry \"$name\" " . $options . " {\n";
+    if ($saveDefault) {
+        $conf .= "  savedefault\n";
     }
+    $conf .= $grubBoot->search . "\n";
+    if ($copyKernels == 0) {
+        $conf .= $grubStore->search . "\n";
+    }
+    $conf .= "  $extraPerEntryConfig\n" if $extraPerEntryConfig;
+    $conf .= "  multiboot $xen $xenParams\n" if $xen;
+    $conf .= "  " . ($xen ? "module" : "linux") . " $kernel $kernelParams\n";
+    $conf .= "  " . ($xen ? "module" : "initrd") . " $initrd\n";
+    $conf .= "}\n\n";
 }
 
 
 # Add default entries.
 $conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
 
-addEntry("NixOS - Default", $defaultConfig, $entryOptions);
+addEntry("@distroName@ - Default", $defaultConfig, $entryOptions, 1);
 
 $conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
 
@@ -536,7 +518,7 @@ foreach my $link (@links) {
         my $linkname = basename($link);
         $entryName = "($linkname - $date - $version)";
     }
-    addEntry("NixOS - $entryName", $link);
+    addEntry("@distroName@ - $entryName", $link, "", 1);
 }
 
 my $grubBootPath = $grubBoot->path;
@@ -548,7 +530,7 @@ sub addProfile {
     my ($profile, $description) = @_;
 
     # Add entries for all generations of this profile.
-    $conf .= "submenu \"$description\" --class submenu {\n" if $grubVersion == 2;
+    $conf .= "submenu \"$description\" --class submenu {\n";
 
     sub nrFromGen { my ($x) = @_; $x =~ /\/\w+-(\d+)-link/; return $1; }
 
@@ -568,20 +550,18 @@ sub addProfile {
             -e "$link/nixos-version"
             ? readFile("$link/nixos-version")
             : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
-        addEntry("NixOS - Configuration " . nrFromGen($link) . " ($date - $version)", $link, $subEntryOptions);
+        addEntry("@distroName@ - Configuration " . nrFromGen($link) . " ($date - $version)", $link, $subEntryOptions, 0);
     }
 
-    $conf .= "}\n" if $grubVersion == 2;
+    $conf .= "}\n";
 }
 
-addProfile "/nix/var/nix/profiles/system", "NixOS - All configurations";
+addProfile "/nix/var/nix/profiles/system", "@distroName@ - All configurations";
 
-if ($grubVersion == 2) {
-    for my $profile (glob "/nix/var/nix/profiles/system-profiles/*") {
-        my $name = basename($profile);
-        next unless $name =~ /^\w+$/;
-        addProfile $profile, "NixOS - Profile '$name'";
-    }
+for my $profile (glob "/nix/var/nix/profiles/system-profiles/*") {
+    my $name = basename($profile);
+    next unless $name =~ /^\w+$/;
+    addProfile $profile, "@distroName@ - Profile '$name'";
 }
 
 # extraPrepareConfig could refer to @bootPath@, which we have to substitute
@@ -593,22 +573,20 @@ if ($extraPrepareConfig ne "") {
 }
 
 # write the GRUB config.
-my $confFile = $grubVersion == 1 ? "$bootPath/grub/menu.lst" : "$bootPath/grub/grub.cfg";
+my $confFile = "$bootPath/grub/grub.cfg";
 my $tmpFile = $confFile . ".tmp";
 writeFile($tmpFile, $conf);
 
 
 # check whether to install GRUB EFI or not
 sub getEfiTarget {
-    if ($grubVersion == 1) {
-        return "no"
-    } elsif (($grub ne "") && ($grubEfi ne "")) {
+    if (($grub ne "") && ($grubEfi ne "")) {
         # EFI can only be installed when target is set;
         # A target is also required then for non-EFI grub
         if (($grubTarget eq "") || ($grubTargetEfi eq "")) { die }
         else { return "both" }
     } elsif (($grub ne "") && ($grubEfi eq "")) {
-        # TODO: It would be safer to disallow non-EFI grub installation if no taget is given.
+        # TODO: It would be safer to disallow non-EFI grub installation if no target is given.
         #       If no target is given, then grub auto-detects the target which can lead to errors.
         #       E.g. it seems as if grub would auto-detect a EFI target based on the availability
         #       of a EFI partition.
@@ -727,7 +705,7 @@ symlink "$bootPath", "$tmpDir/boot" or die "Failed to symlink $tmpDir/boot: $!";
 if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) {
     foreach my $dev (@deviceTargets) {
         next if $dev eq "nodev";
-        print STDERR "installing the GRUB $grubVersion boot loader on $dev...\n";
+        print STDERR "installing the GRUB 2 boot loader on $dev...\n";
         my @command = ("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", Cwd::abs_path($dev), @extraGrubInstallArgs);
         if ($forceInstall eq "true") {
             push @command, "--force";
@@ -742,7 +720,7 @@ if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) {
 
 # install EFI GRUB
 if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) {
-    print STDERR "installing the GRUB $grubVersion EFI boot loader into $efiSysMountPoint...\n";
+    print STDERR "installing the GRUB 2 boot loader into $efiSysMountPoint...\n";
     my @command = ("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint", @extraGrubInstallArgs);
     if ($forceInstall eq "true") {
         push @command, "--force";
diff --git a/nixos/modules/system/boot/loader/grub/ipxe.nix b/nixos/modules/system/boot/loader/grub/ipxe.nix
index adddcbee016..d926b7ceaa6 100644
--- a/nixos/modules/system/boot/loader/grub/ipxe.nix
+++ b/nixos/modules/system/boot/loader/grub/ipxe.nix
@@ -46,11 +46,7 @@ in
 
   config = mkIf (builtins.length scripts != 0) {
 
-    boot.loader.grub.extraEntries =
-      if config.boot.loader.grub.version == 2 then
-        toString (map grubEntry scripts)
-      else
-        throw "iPXE is not supported with GRUB 1.";
+    boot.loader.grub.extraEntries = toString (map grubEntry scripts);
 
     boot.loader.grub.extraFiles =
       { "ipxe.lkrn" = "${pkgs.ipxe}/ipxe.lkrn"; }
diff --git a/nixos/modules/system/boot/loader/grub/memtest.nix b/nixos/modules/system/boot/loader/grub/memtest.nix
index ccb6e8cc3ca..ee969e9bff5 100644
--- a/nixos/modules/system/boot/loader/grub/memtest.nix
+++ b/nixos/modules/system/boot/loader/grub/memtest.nix
@@ -84,15 +84,11 @@ in
     })
 
     (mkIf (cfg.enable && !efiSupport) {
-      boot.loader.grub.extraEntries =
-        if config.boot.loader.grub.version == 2 then
-          ''
-            menuentry "Memtest86+" {
-              linux16 @bootRoot@/memtest.bin ${toString cfg.params}
-            }
-          ''
-        else
-          throw "Memtest86+ is not supported with GRUB 1.";
+      boot.loader.grub.extraEntries = ''
+        menuentry "Memtest86+" {
+          linux16 @bootRoot@/memtest.bin ${toString cfg.params}
+        }
+      '';
 
       boot.loader.grub.extraFiles."memtest.bin" = "${memtest86}/memtest.bin";
     })
diff --git a/nixos/modules/system/boot/loader/init-script/init-script-builder.sh b/nixos/modules/system/boot/loader/init-script/init-script-builder.sh
index bd3fc64999d..755ea259c42 100644
--- a/nixos/modules/system/boot/loader/init-script/init-script-builder.sh
+++ b/nixos/modules/system/boot/loader/init-script/init-script-builder.sh
@@ -64,13 +64,13 @@ addEntry() {
 
 mkdir -p /boot /sbin
 
-addEntry "NixOS - Default" $defaultConfig ""
+addEntry "@distroName@ - Default" $defaultConfig ""
 
 # Add all generations of the system profile to the menu, in reverse
 # (most recent to least recent) order.
 for link in $((ls -d $defaultConfig/specialisation/* ) | sort -n); do
     date=$(stat --printf="%y\n" $link | sed 's/\..*//')
-    addEntry "NixOS - variation" $link ""
+    addEntry "@distroName@ - variation" $link ""
 done
 
 for generation in $(
@@ -85,7 +85,7 @@ for generation in $(
     else
       suffix="($date)"
     fi
-    addEntry "NixOS - Configuration $generation $suffix" $link "$generation ($date)"
+    addEntry "@distroName@ - Configuration $generation $suffix" $link "$generation ($date)"
 done
 
 mv $tmpOther $targetOther
diff --git a/nixos/modules/system/boot/loader/init-script/init-script.nix b/nixos/modules/system/boot/loader/init-script/init-script.nix
index 8287131d321..4d33ed6b665 100644
--- a/nixos/modules/system/boot/loader/init-script/init-script.nix
+++ b/nixos/modules/system/boot/loader/init-script/init-script.nix
@@ -8,6 +8,7 @@ let
     src = ./init-script-builder.sh;
     isExecutable = true;
     inherit (pkgs) bash;
+    inherit (config.system.nixos) distroName;
     path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep];
   };
 
diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
index 1dde5507433..9c9bee93de8 100644
--- a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
+++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
@@ -52,6 +52,10 @@ in
           Whether to create files with the system generations in
           `/boot`.
           `/boot/old` will hold files from old generations.
+
+          ::: {.note}
+          These options are deprecated, unsupported, and may not work like expected.
+          :::
         '';
       };
 
@@ -67,6 +71,10 @@ in
           type = types.bool;
           description = lib.mdDoc ''
             Enable using uboot as bootmanager for the raspberry pi.
+
+            ::: {.note}
+            These options are deprecated, unsupported, and may not work like expected.
+            :::
           '';
         };
 
@@ -76,6 +84,10 @@ in
           type = types.int;
           description = lib.mdDoc ''
             Maximum number of configurations in the boot menu.
+
+            ::: {.note}
+            These options are deprecated, unsupported, and may not work like expected.
+            :::
           '';
         };
 
@@ -87,19 +99,53 @@ in
         description = lib.mdDoc ''
           Extra options that will be appended to `/boot/config.txt` file.
           For possible values, see: https://www.raspberrypi.com/documentation/computers/config_txt.html
+
+          ::: {.note}
+          These options are deprecated, unsupported, and may not work like expected.
+          :::
         '';
       };
     };
   };
 
-  config = mkIf cfg.enable {
-    assertions = singleton {
-      assertion = !pkgs.stdenv.hostPlatform.isAarch64 || cfg.version >= 3;
-      message = "Only Raspberry Pi >= 3 supports aarch64.";
-    };
+  config = mkMerge[
+    (mkIf cfg.uboot.enable {
+      warnings = [
+        ''
+          The option set for `boot.loader.raspberrypi.uboot` has been recommended against
+          for years, and is now formally deprecated.
+
+          It is possible it already did not work like you expected.
+
+          It never worked on the Raspberry Pi 4 family.
+
+          These options will be removed by NixOS 24.11.
+        ''
+      ];
+    })
+    (mkIf cfg.enable {
+      warnings = [
+        ''
+          The option set for `boot.loader.raspberrypi` has been recommended against
+          for years, and is now formally deprecated.
+
+          It is possible it already did not work like you expected.
+
+          It never worked on the Raspberry Pi 4 family.
+
+          These options will be removed by NixOS 24.11.
+        ''
+      ];
+    })
+    (mkIf cfg.enable {
+      assertions = singleton {
+        assertion = !pkgs.stdenv.hostPlatform.isAarch64 || cfg.version >= 3;
+        message = "Only Raspberry Pi >= 3 supports aarch64.";
+      };
 
-    system.build.installBootLoader = builder;
-    system.boot.loader.id = "raspberrypi";
-    system.boot.loader.kernelFile = pkgs.stdenv.hostPlatform.linux-kernel.target;
-  };
+      system.build.installBootLoader = builder;
+      system.boot.loader.id = "raspberrypi";
+      system.boot.loader.kernelFile = pkgs.stdenv.hostPlatform.linux-kernel.target;
+    })
+  ];
 }
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
index ad7e2184d2a..a040518a5a5 100755
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
@@ -42,7 +42,7 @@ def system_dir(profile: Optional[str], generation: int, specialisation: Optional
     else:
         return d
 
-BOOT_ENTRY = """title NixOS{profile}{specialisation}
+BOOT_ENTRY = """title {title}
 version Generation {generation} {description}
 linux {kernel}
 initrd {initrd}
@@ -85,51 +85,64 @@ def copy_from_profile(profile: Optional[str], generation: int, specialisation: O
     return efi_file_path
 
 
-def describe_generation(generation_dir: str) -> str:
+def describe_generation(profile: Optional[str], generation: int, specialisation: Optional[str]) -> str:
     try:
-        with open("%s/nixos-version" % generation_dir) as f:
+        with open(profile_path(profile, generation, specialisation, "nixos-version")) as f:
             nixos_version = f.read()
     except IOError:
         nixos_version = "Unknown"
 
-    kernel_dir = os.path.dirname(os.path.realpath("%s/kernel" % generation_dir))
+    kernel_dir = os.path.dirname(profile_path(profile, generation, specialisation, "kernel"))
     module_dir = glob.glob("%s/lib/modules/*" % kernel_dir)[0]
     kernel_version = os.path.basename(module_dir)
 
-    build_time = int(os.path.getctime(generation_dir))
+    build_time = int(os.path.getctime(system_dir(profile, generation, specialisation)))
     build_date = datetime.datetime.fromtimestamp(build_time).strftime('%F')
 
-    description = "NixOS {}, Linux Kernel {}, Built on {}".format(
+    description = "@distroName@ {}, Linux Kernel {}, Built on {}".format(
         nixos_version, kernel_version, build_date
     )
 
     return description
 
 
-def write_entry(profile: Optional[str], generation: int, specialisation: Optional[str], machine_id: str) -> None:
+def write_entry(profile: Optional[str], generation: int, specialisation: Optional[str],
+                machine_id: str, current: bool) -> None:
     kernel = copy_from_profile(profile, generation, specialisation, "kernel")
     initrd = copy_from_profile(profile, generation, specialisation, "initrd")
+
+    title = "@distroName@{profile}{specialisation}".format(
+        profile=" [" + profile + "]" if profile else "",
+        specialisation=" (%s)" % specialisation if specialisation else "")
+
     try:
         append_initrd_secrets = profile_path(profile, generation, specialisation, "append-initrd-secrets")
         subprocess.check_call([append_initrd_secrets, "@efiSysMountPoint@%s" % (initrd)])
     except FileNotFoundError:
         pass
+    except subprocess.CalledProcessError:
+        if current:
+            print("failed to create initrd secrets!", file=sys.stderr)
+            sys.exit(1)
+        else:
+            print("warning: failed to create initrd secrets "
+                  f'for "{title} - Configuration {generation}", an older generation', file=sys.stderr)
+            print("note: this is normal after having removed "
+                  "or renamed a file in `boot.initrd.secrets`", file=sys.stderr)
     entry_file = "@efiSysMountPoint@/loader/entries/%s" % (
         generation_conf_filename(profile, generation, specialisation))
-    generation_dir = os.readlink(system_dir(profile, generation, specialisation))
     tmp_path = "%s.tmp" % (entry_file)
-    kernel_params = "init=%s/init " % generation_dir
+    kernel_params = "init=%s " % profile_path(profile, generation, specialisation, "init")
 
-    with open("%s/kernel-params" % (generation_dir)) as params_file:
+    with open(profile_path(profile, generation, specialisation, "kernel-params")) as params_file:
         kernel_params = kernel_params + params_file.read()
     with open(tmp_path, 'w') as f:
-        f.write(BOOT_ENTRY.format(profile=" [" + profile + "]" if profile else "",
-                    specialisation=" (%s)" % specialisation if specialisation else "",
+        f.write(BOOT_ENTRY.format(title=title,
                     generation=generation,
                     kernel=kernel,
                     initrd=initrd,
                     kernel_params=kernel_params,
-                    description=describe_generation(generation_dir)))
+                    description=describe_generation(profile, generation, specialisation)))
         if machine_id is not None:
             f.write("machine-id %s\n" % machine_id)
     os.rename(tmp_path, entry_file)
@@ -206,8 +219,8 @@ def get_profiles() -> List[str]:
         return []
 
 def main() -> None:
-    parser = argparse.ArgumentParser(description='Update NixOS-related systemd-boot files')
-    parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help='The default NixOS config to boot')
+    parser = argparse.ArgumentParser(description='Update @distroName@-related systemd-boot files')
+    parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help='The default @distroName@ config to boot')
     args = parser.parse_args()
 
     try:
@@ -228,20 +241,21 @@ def main() -> None:
         warnings.warn("NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER", DeprecationWarning)
         os.environ["NIXOS_INSTALL_BOOTLOADER"] = "1"
 
+    # flags to pass to bootctl install/update
+    bootctl_flags = []
+
+    if "@canTouchEfiVariables@" != "1":
+        bootctl_flags.append("--no-variables")
+
+    if "@graceful@" == "1":
+        bootctl_flags.append("--graceful")
+
     if os.getenv("NIXOS_INSTALL_BOOTLOADER") == "1":
         # bootctl uses fopen() with modes "wxe" and fails if the file exists.
         if os.path.exists("@efiSysMountPoint@/loader/loader.conf"):
             os.unlink("@efiSysMountPoint@/loader/loader.conf")
 
-        flags = []
-
-        if "@canTouchEfiVariables@" != "1":
-            flags.append("--no-variables")
-
-        if "@graceful@" == "1":
-            flags.append("--graceful")
-
-        subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + flags + ["install"])
+        subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + bootctl_flags + ["install"])
     else:
         # Update bootloader to latest if needed
         available_out = subprocess.check_output(["@systemd@/bin/bootctl", "--version"], universal_newlines=True).split()[2]
@@ -270,7 +284,7 @@ def main() -> None:
                 print("skipping systemd-boot update to %s because of known regression" % available_version)
             else:
                 print("updating systemd-boot from %s to %s" % (installed_version, available_version))
-                subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@", "update"])
+                subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + bootctl_flags + ["update"])
 
     mkdir_p("@efiSysMountPoint@/efi/nixos")
     mkdir_p("@efiSysMountPoint@/loader/entries")
@@ -281,14 +295,19 @@ def main() -> None:
     remove_old_entries(gens)
     for gen in gens:
         try:
-            write_entry(*gen, machine_id)
+            is_default = os.path.dirname(profile_path(*gen, "init")) == args.default_config
+            write_entry(*gen, machine_id, current=is_default)
             for specialisation in get_specialisations(*gen):
-                write_entry(*specialisation, machine_id)
-            if os.readlink(system_dir(*gen)) == args.default_config:
+                write_entry(*specialisation, machine_id, current=is_default)
+            if is_default:
                 write_loader_conf(*gen)
         except OSError as e:
-            profile = f"profile '{gen.profile}'" if gen.profile else "default profile"
-            print("ignoring {} in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr)
+            # See https://github.com/NixOS/nixpkgs/issues/114552
+            if e.errno == errno.EINVAL:
+                profile = f"profile '{gen.profile}'" if gen.profile else "default profile"
+                print("ignoring {} in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr)
+            else:
+                raise e
 
     for root, _, files in os.walk('@efiSysMountPoint@/efi/nixos/.extra-files', topdown=False):
         relative_root = root.removeprefix("@efiSysMountPoint@/efi/nixos/.extra-files").removeprefix("/")
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
index 103d6e583c3..8a3e89e5888 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
@@ -20,7 +20,7 @@ let
 
     nix = config.nix.package.out;
 
-    timeout = if config.boot.loader.timeout != null then config.boot.loader.timeout else "";
+    timeout = optionalString (config.boot.loader.timeout != null) config.boot.loader.timeout;
 
     editor = if cfg.editor then "True" else "False";
 
@@ -30,9 +30,11 @@ let
 
     inherit (efi) efiSysMountPoint canTouchEfiVariables;
 
-    memtest86 = if cfg.memtest86.enable then pkgs.memtest86-efi else "";
+    inherit (config.system.nixos) distroName;
 
-    netbootxyz = if cfg.netbootxyz.enable then pkgs.netbootxyz-efi else "";
+    memtest86 = optionalString cfg.memtest86.enable pkgs.memtest86-efi;
+
+    netbootxyz = optionalString cfg.netbootxyz.enable pkgs.netbootxyz-efi;
 
     copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
       empty_file=$(${pkgs.coreutils}/bin/mktemp)
diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix
index 03d03cb348e..dc3fe163116 100644
--- a/nixos/modules/system/boot/luksroot.nix
+++ b/nixos/modules/system/boot/luksroot.nix
@@ -130,7 +130,7 @@ let
     ''}
 
     # Disable all input echo for the whole stage. We could use read -s
-    # instead but that would ocasionally leak characters between read
+    # instead but that would occasionally leak characters between read
     # invocations.
     stty -echo
   '';
@@ -158,6 +158,20 @@ let
       wait_target "header" ${dev.header} || die "${dev.header} is unavailable"
     ''}
 
+    try_empty_passphrase() {
+        ${if dev.tryEmptyPassphrase then ''
+             echo "Trying empty passphrase!"
+             echo "" | ${csopen}
+             cs_status=$?
+             if [ $cs_status -eq 0 ]; then
+                 return 0
+             else
+                 return 1
+             fi
+        '' else "return 1"}
+    }
+
+
     do_open_passphrase() {
         local passphrase
 
@@ -212,13 +226,27 @@ let
             ${csopen} --key-file=${dev.keyFile} \
               ${optionalString (dev.keyFileSize != null) "--keyfile-size=${toString dev.keyFileSize}"} \
               ${optionalString (dev.keyFileOffset != null) "--keyfile-offset=${toString dev.keyFileOffset}"}
+            cs_status=$?
+            if [ $cs_status -ne 0 ]; then
+              echo "Key File ${dev.keyFile} failed!"
+              if ! try_empty_passphrase; then
+                ${if dev.fallbackToPassword then "echo" else "die"} "${dev.keyFile} is unavailable"
+                echo " - failing back to interactive password prompt"
+                do_open_passphrase
+              fi
+            fi
         else
-            ${if dev.fallbackToPassword then "echo" else "die"} "${dev.keyFile} is unavailable"
-            echo " - failing back to interactive password prompt"
-            do_open_passphrase
+            # If the key file never shows up we should also try the empty passphrase
+            if ! try_empty_passphrase; then
+               ${if dev.fallbackToPassword then "echo" else "die"} "${dev.keyFile} is unavailable"
+               echo " - failing back to interactive password prompt"
+               do_open_passphrase
+            fi
         fi
         '' else ''
-        do_open_passphrase
+           if ! try_empty_passphrase; then
+              do_open_passphrase
+           fi
         ''}
     }
 
@@ -476,6 +504,7 @@ let
   preLVM = filterAttrs (n: v: v.preLVM) luks.devices;
   postLVM = filterAttrs (n: v: !v.preLVM) luks.devices;
 
+
   stage1Crypttab = pkgs.writeText "initrd-crypttab" (lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: let
     opts = v.crypttabExtraOpts
       ++ optional v.allowDiscards "discard"
@@ -483,6 +512,8 @@ let
       ++ optional (v.header != null) "header=${v.header}"
       ++ optional (v.keyFileOffset != null) "keyfile-offset=${toString v.keyFileOffset}"
       ++ optional (v.keyFileSize != null) "keyfile-size=${toString v.keyFileSize}"
+      ++ optional (v.keyFileTimeout != null) "keyfile-timeout=${builtins.toString v.keyFileTimeout}s"
+      ++ optional (v.tryEmptyPassphrase) "try-empty-password=true"
     ;
   in "${n} ${v.device} ${if v.keyFile == null then "-" else v.keyFile} ${lib.concatStringsSep "," opts}") luks.devices));
 
@@ -594,6 +625,25 @@ in
             '';
           };
 
+          tryEmptyPassphrase = mkOption {
+            default = false;
+            type = types.bool;
+            description = lib.mdDoc ''
+              If keyFile fails then try an empty passphrase first before
+              prompting for password.
+            '';
+          };
+
+          keyFileTimeout = mkOption {
+            default = null;
+            example = 5;
+            type = types.nullOr types.int;
+            description = lib.mdDoc ''
+              The amount of time in seconds for a keyFile to appear before
+              timing out and trying passwords.
+            '';
+          };
+
           keyFileSize = mkOption {
             default = null;
             example = 4096;
@@ -811,7 +861,7 @@ in
             '';
             description = lib.mdDoc ''
               Commands that should be run right before we try to mount our LUKS device.
-              This can be useful, if the keys needed to open the drive is on another partion.
+              This can be useful, if the keys needed to open the drive is on another partition.
             '';
           };
 
@@ -889,6 +939,10 @@ in
           message = "boot.initrd.luks.devices.<name>.bypassWorkqueues is not supported for kernels older than 5.9";
         }
 
+        { assertion = !config.boot.initrd.systemd.enable -> all (x: x.keyFileTimeout == null) (attrValues luks.devices);
+          message = "boot.initrd.luks.devices.<name>.keyFileTimeout is only supported for systemd initrd";
+        }
+
         { assertion = config.boot.initrd.systemd.enable -> all (dev: !dev.fallbackToPassword) (attrValues luks.devices);
           message = "boot.initrd.luks.devices.<name>.fallbackToPassword is implied by systemd stage 1.";
         }
@@ -926,10 +980,17 @@ in
       ++ luks.cryptoModules
       # workaround until https://marc.info/?l=linux-crypto-vger&m=148783562211457&w=4 is merged
       # remove once 'modprobe --show-depends xts' shows ecb as a dependency
-      ++ (if builtins.elem "xts" luks.cryptoModules then ["ecb"] else []);
+      ++ (optional (builtins.elem "xts" luks.cryptoModules) "ecb");
 
     # copy the cryptsetup binary and it's dependencies
-    boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
+    boot.initrd.extraUtilsCommands = let
+      pbkdf2-sha512 = pkgs.runCommandCC "pbkdf2-sha512" { buildInputs = [ pkgs.openssl ]; } ''
+        mkdir -p "$out/bin"
+        cc -O3 -lcrypto ${./pbkdf2-sha512.c} -o "$out/bin/pbkdf2-sha512"
+        strip -s "$out/bin/pbkdf2-sha512"
+      '';
+    in
+    mkIf (!config.boot.initrd.systemd.enable) ''
       copy_bin_and_libs ${pkgs.cryptsetup}/bin/cryptsetup
       copy_bin_and_libs ${askPass}/bin/cryptsetup-askpass
       sed -i s,/bin/sh,$out/bin/sh, $out/bin/cryptsetup-askpass
@@ -939,9 +1000,7 @@ in
         copy_bin_and_libs ${pkgs.yubikey-personalization}/bin/ykinfo
         copy_bin_and_libs ${pkgs.openssl.bin}/bin/openssl
 
-        cc -O3 -I${pkgs.openssl.dev}/include -L${lib.getLib pkgs.openssl}/lib ${./pbkdf2-sha512.c} -o pbkdf2-sha512 -lcrypto
-        strip -s pbkdf2-sha512
-        copy_bin_and_libs pbkdf2-sha512
+        copy_bin_and_libs ${pbkdf2-sha512}/bin/pbkdf2-sha512
 
         mkdir -p $out/etc/ssl
         cp -pdv ${pkgs.openssl.out}/etc/ssl/openssl.cnf $out/etc/ssl
@@ -965,13 +1024,12 @@ in
         copy_bin_and_libs ${pkgs.gnupg}/libexec/scdaemon
 
         ${concatMapStringsSep "\n" (x:
-          if x.gpgCard != null then
+          optionalString (x.gpgCard != null)
             ''
               mkdir -p $out/secrets/gpg-keys/${x.device}
               cp -a ${x.gpgCard.encryptedPass} $out/secrets/gpg-keys/${x.device}/cryptkey.gpg
               cp -a ${x.gpgCard.publicKey} $out/secrets/gpg-keys/${x.device}/pubkey.asc
             ''
-          else ""
           ) (attrValues luks.devices)
         }
       ''}
diff --git a/nixos/modules/system/boot/modprobe.nix b/nixos/modules/system/boot/modprobe.nix
index 54bb7ea9ddd..d751c4462d3 100644
--- a/nixos/modules/system/boot/modprobe.nix
+++ b/nixos/modules/system/boot/modprobe.nix
@@ -7,7 +7,7 @@ with lib;
   ###### interface
 
   options = {
-    boot.modprobeConfig.enable = mkEnableOption (lib.mdDoc "modprobe config. This is useful for systemds like containers which do not require a kernel.") // {
+    boot.modprobeConfig.enable = mkEnableOption (lib.mdDoc "modprobe config. This is useful for systems like containers which do not require a kernel") // {
       default = true;
     };
 
diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
index 188f2f64dc8..c5fc259e268 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -2,12 +2,11 @@
 
 with utils.systemdUtils.unitOptions;
 with utils.systemdUtils.lib;
+with utils.systemdUtils.network.units;
 with lib;
 
 let
 
-  cfg = config.systemd.network;
-
   check = {
 
     global = {
@@ -27,9 +26,11 @@ let
 
       sectionDHCPv4 = checkUnitConfig "DHCPv4" [
         (assertOnlyFields [
+          "ClientIdentifier"
           "DUIDType"
           "DUIDRawData"
         ])
+        (assertValueOneOf "ClientIdentifier" ["mac" "duid" "duid-only"])
       ];
 
       sectionDHCPv6 = checkUnitConfig "DHCPv6" [
@@ -72,6 +73,9 @@ let
           "CombinedChannels"
           "RxBufferSize"
           "TxBufferSize"
+          "ReceiveQueues"
+          "TransmitQueues"
+          "TransmitQueueLength"
         ])
         (assertValueOneOf "MACAddressPolicy" ["persistent" "random" "none"])
         (assertMacAddress "MACAddress")
@@ -98,6 +102,9 @@ let
         (assertRange "CombinedChannels" 1 4294967295)
         (assertInt "RxBufferSize")
         (assertInt "TxBufferSize")
+        (assertRange "ReceiveQueues" 1 4096)
+        (assertRange "TransmitQueues" 1 4096)
+        (assertRange "TransmitQueueLength" 1 4294967294)
       ];
     };
 
@@ -303,6 +310,48 @@ let
 
       sectionTap = checkUnitConfig "Tap" tunChecks;
 
+      sectionL2TP = checkUnitConfig "L2TP" [
+        (assertOnlyFields [
+          "TunnelId"
+          "PeerTunnelId"
+          "Remote"
+          "Local"
+          "EncapsulationType"
+          "UDPSourcePort"
+          "UDPDestinationPort"
+          "UDPChecksum"
+          "UDP6ZeroChecksumTx"
+          "UDP6ZeroChecksumRx"
+        ])
+        (assertInt "TunnelId")
+        (assertRange "TunnelId" 1 4294967295)
+        (assertInt "PeerTunnelId")
+        (assertRange "PeerTunnelId" 1 4294967295)
+        (assertValueOneOf "EncapsulationType" [ "ip" "udp" ])
+        (assertPort "UDPSourcePort")
+        (assertPort "UDPDestinationPort")
+        (assertValueOneOf "UDPChecksum" boolValues)
+        (assertValueOneOf "UDP6ZeroChecksumTx" boolValues)
+        (assertValueOneOf "UDP6ZeroChecksumRx" boolValues)
+      ];
+
+      sectionL2TPSession = checkUnitConfig "L2TPSession" [
+        (assertOnlyFields [
+          "Name"
+          "SessionId"
+          "PeerSessionId"
+          "Layer2SpecificHeader"
+        ])
+        (assertHasField "Name")
+        (assertHasField "SessionId")
+        (assertInt "SessionId")
+        (assertRange "SessionId" 1 4294967295)
+        (assertHasField "PeerSessionId")
+        (assertInt "PeerSessionId")
+        (assertRange "PeerSessionId" 1 4294967295)
+        (assertValueOneOf "Layer2SpecificHeader" [ "none" "default" ])
+      ];
+
       # NOTE The PrivateKey directive is missing on purpose here, please
       # do not add it to this list. The nix store is world-readable let's
       # refrain ourselves from providing a footgun.
@@ -533,6 +582,7 @@ let
           "VLAN"
           "IPVLAN"
           "MACVLAN"
+          "MACVTAP"
           "VXLAN"
           "Tunnel"
           "MACsec"
@@ -918,6 +968,470 @@ let
         (assertMacAddress "MACAddress")
       ];
 
+      sectionBridge = checkUnitConfig "Bridge" [
+        (assertOnlyFields [
+          "UnicastFlood"
+          "MulticastFlood"
+          "MulticastToUnicast"
+          "NeighborSuppression"
+          "Learning"
+          "Hairpin"
+          "Isolated"
+          "UseBPDU"
+          "FastLeave"
+          "AllowPortToBeRoot"
+          "ProxyARP"
+          "ProxyARPWiFi"
+          "MulticastRouter"
+          "Cost"
+          "Priority"
+        ])
+        (assertValueOneOf "UnicastFlood" boolValues)
+        (assertValueOneOf "MulticastFlood" boolValues)
+        (assertValueOneOf "MulticastToUnicast" boolValues)
+        (assertValueOneOf "NeighborSuppression" boolValues)
+        (assertValueOneOf "Learning" boolValues)
+        (assertValueOneOf "Hairpin" boolValues)
+        (assertValueOneOf "Isolated" boolValues)
+        (assertValueOneOf "UseBPDU" boolValues)
+        (assertValueOneOf "FastLeave" boolValues)
+        (assertValueOneOf "AllowPortToBeRoot" boolValues)
+        (assertValueOneOf "ProxyARP" boolValues)
+        (assertValueOneOf "ProxyARPWiFi" boolValues)
+        (assertValueOneOf "MulticastRouter" [ "no" "query" "permanent" "temporary" ])
+        (assertInt "Cost")
+        (assertRange "Cost" 1 65535)
+        (assertInt "Priority")
+        (assertRange "Priority" 0 63)
+      ];
+
+      sectionBridgeFDB = checkUnitConfig "BridgeFDB" [
+        (assertOnlyFields [
+          "MACAddress"
+          "Destination"
+          "VLANId"
+          "VNI"
+          "AssociatedWith"
+          "OutgoingInterface"
+        ])
+        (assertHasField "MACAddress")
+        (assertInt "VLANId")
+        (assertRange "VLANId" 0 4094)
+        (assertInt "VNI")
+        (assertRange "VNI" 1 16777215)
+        (assertValueOneOf "AssociatedWith" [ "use" "self" "master" "router" ])
+      ];
+
+      sectionBridgeMDB = checkUnitConfig "BridgeMDB" [
+        (assertOnlyFields [
+          "MulticastGroupAddress"
+          "VLANId"
+        ])
+        (assertHasField "MulticastGroupAddress")
+        (assertInt "VLANId")
+        (assertRange "VLANId" 0 4094)
+      ];
+
+      sectionLLDP = checkUnitConfig "LLDP" [
+        (assertOnlyFields [
+          "MUDURL"
+        ])
+      ];
+
+      sectionCAN = checkUnitConfig "CAN" [
+        (assertOnlyFields [
+          "BitRate"
+          "SamplePoint"
+          "TimeQuantaNSec"
+          "PropagationSegment"
+          "PhaseBufferSegment1"
+          "PhaseBufferSegment2"
+          "SyncJumpWidth"
+          "DataBitRate"
+          "DataSamplePoint"
+          "DataTimeQuantaNSec"
+          "DataPropagationSegment"
+          "DataPhaseBufferSegment1"
+          "DataPhaseBufferSegment2"
+          "DataSyncJumpWidth"
+          "FDMode"
+          "FDNonISO"
+          "RestartSec"
+          "Termination"
+          "TripleSampling"
+          "BusErrorReporting"
+          "ListenOnly"
+          "Loopback"
+          "OneShot"
+          "PresumeAck"
+          "ClassicDataLengthCode"
+        ])
+        (assertInt "TimeQuantaNSec" )
+        (assertRange "TimeQuantaNSec" 0 4294967295 )
+        (assertInt "PropagationSegment" )
+        (assertRange "PropagationSegment" 0 4294967295 )
+        (assertInt "PhaseBufferSegment1" )
+        (assertRange "PhaseBufferSegment1" 0 4294967295 )
+        (assertInt "PhaseBufferSegment2" )
+        (assertRange "PhaseBufferSegment2" 0 4294967295 )
+        (assertInt "SyncJumpWidth" )
+        (assertRange "SyncJumpWidth" 0 4294967295 )
+        (assertInt "DataTimeQuantaNSec" )
+        (assertRange "DataTimeQuantaNSec" 0 4294967295 )
+        (assertInt "DataPropagationSegment" )
+        (assertRange "DataPropagationSegment" 0 4294967295 )
+        (assertInt "DataPhaseBufferSegment1" )
+        (assertRange "DataPhaseBufferSegment1" 0 4294967295 )
+        (assertInt "DataPhaseBufferSegment2" )
+        (assertRange "DataPhaseBufferSegment2" 0 4294967295 )
+        (assertInt "DataSyncJumpWidth" )
+        (assertRange "DataSyncJumpWidth" 0 4294967295 )
+        (assertValueOneOf "FDMode" boolValues)
+        (assertValueOneOf "FDNonISO" boolValues)
+        (assertValueOneOf "TripleSampling" boolValues)
+        (assertValueOneOf "BusErrorReporting" boolValues)
+        (assertValueOneOf "ListenOnly" boolValues)
+        (assertValueOneOf "Loopback" boolValues)
+        (assertValueOneOf "OneShot" boolValues)
+        (assertValueOneOf "PresumeAck" boolValues)
+        (assertValueOneOf "ClassicDataLengthCode" boolValues)
+      ];
+
+      sectionIPoIB = checkUnitConfig "IPoIB" [
+        (assertOnlyFields [
+          "Mode"
+          "IgnoreUserspaceMulticastGroup"
+        ])
+        (assertValueOneOf "Mode" [ "datagram" "connected" ])
+        (assertValueOneOf "IgnoreUserspaceMulticastGroup" boolValues)
+      ];
+
+      sectionQDisc = checkUnitConfig "QDisc" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+        ])
+        (assertValueOneOf "Parent" [ "clsact" "ingress" ])
+      ];
+
+      sectionNetworkEmulator = checkUnitConfig "NetworkEmulator" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "DelaySec"
+          "DelayJitterSec"
+          "PacketLimit"
+          "LossRate"
+          "DuplicateRate"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 0 4294967294)
+      ];
+
+      sectionTokenBucketFilter = checkUnitConfig "TokenBucketFilter" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "LatencySec"
+          "LimitBytes"
+          "BurstBytes"
+          "Rate"
+          "MPUBytes"
+          "PeakRate"
+          "MTUBytes"
+        ])
+      ];
+
+      sectionPIE = checkUnitConfig "PIE" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 1 4294967294)
+      ];
+
+      sectionFlowQueuePIE = checkUnitConfig "FlowQueuePIE" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 1 4294967294)
+      ];
+
+      sectionStochasticFairBlue = checkUnitConfig "StochasticFairBlue" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 1 4294967294)
+      ];
+
+      sectionStochasticFairnessQueueing = checkUnitConfig "StochasticFairnessQueueing" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PerturbPeriodSec"
+        ])
+        (assertInt "PerturbPeriodSec")
+      ];
+
+      sectionBFIFO = checkUnitConfig "BFIFO" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "LimitBytes"
+        ])
+      ];
+
+      sectionPFIFO = checkUnitConfig "PFIFO" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 0 4294967294)
+      ];
+
+      sectionPFIFOHeadDrop = checkUnitConfig "PFIFOHeadDrop" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 0 4294967294)
+      ];
+
+      sectionPFIFOFast = checkUnitConfig "PFIFOFast" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+        ])
+      ];
+
+      sectionCAKE = checkUnitConfig "CAKE" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "Bandwidth"
+          "AutoRateIngress"
+          "OverheadBytes"
+          "MPUBytes"
+          "CompensationMode"
+          "UseRawPacketSize"
+          "FlowIsolationMode"
+          "NAT"
+          "PriorityQueueingPreset"
+          "FirewallMark"
+          "Wash"
+          "SplitGSO"
+        ])
+        (assertValueOneOf "AutoRateIngress" boolValues)
+        (assertInt "OverheadBytes")
+        (assertRange "OverheadBytes" (-64) 256)
+        (assertInt "MPUBytes")
+        (assertRange "MPUBytes" 1 256)
+        (assertValueOneOf "CompensationMode" [ "none" "atm" "ptm" ])
+        (assertValueOneOf "UseRawPacketSize" boolValues)
+        (assertValueOneOf "FlowIsolationMode"
+          [
+            "none"
+            "src-host"
+            "dst-host"
+            "hosts"
+            "flows"
+            "dual-src-host"
+            "dual-dst-host"
+            "triple"
+          ])
+        (assertValueOneOf "NAT" boolValues)
+        (assertValueOneOf "PriorityQueueingPreset"
+          [
+            "besteffort"
+            "precedence"
+            "diffserv8"
+            "diffserv4"
+            "diffserv3"
+          ])
+        (assertInt "FirewallMark")
+        (assertRange "FirewallMark" 1 4294967295)
+        (assertValueOneOf "Wash" boolValues)
+        (assertValueOneOf "SplitGSO" boolValues)
+      ];
+
+      sectionControlledDelay = checkUnitConfig "ControlledDelay" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+          "TargetSec"
+          "IntervalSec"
+          "ECN"
+          "CEThresholdSec"
+        ])
+        (assertValueOneOf "ECN" boolValues)
+      ];
+
+      sectionDeficitRoundRobinScheduler = checkUnitConfig "DeficitRoundRobinScheduler" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+        ])
+      ];
+
+      sectionDeficitRoundRobinSchedulerClass = checkUnitConfig "DeficitRoundRobinSchedulerClass" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "QuantumBytes"
+        ])
+      ];
+
+      sectionEnhancedTransmissionSelection = checkUnitConfig "EnhancedTransmissionSelection" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "Bands"
+          "StrictBands"
+          "QuantumBytes"
+          "PriorityMap"
+        ])
+        (assertInt "Bands")
+        (assertRange "Bands" 1 16)
+        (assertInt "StrictBands")
+        (assertRange "StrictBands" 1 16)
+      ];
+
+      sectionGenericRandomEarlyDetection = checkUnitConfig "GenericRandomEarlyDetection" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "VirtualQueues"
+          "DefaultVirtualQueue"
+          "GenericRIO"
+        ])
+        (assertInt "VirtualQueues")
+        (assertRange "VirtualQueues" 1 16)
+        (assertInt "DefaultVirtualQueue")
+        (assertRange "DefaultVirtualQueue" 1 16)
+        (assertValueOneOf "GenericRIO" boolValues)
+      ];
+
+      sectionFairQueueingControlledDelay = checkUnitConfig "FairQueueingControlledDelay" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+          "MemoryLimitBytes"
+          "Flows"
+          "TargetSec"
+          "IntervalSec"
+          "QuantumBytes"
+          "ECN"
+          "CEThresholdSec"
+        ])
+        (assertInt "PacketLimit")
+        (assertInt "Flows")
+        (assertValueOneOf "ECN" boolValues)
+      ];
+
+      sectionFairQueueing = checkUnitConfig "FairQueueing" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+          "FlowLimit"
+          "QuantumBytes"
+          "InitualQuantumBytes"
+          "MaximumRate"
+          "Buckets"
+          "OrphanMask"
+          "Pacing"
+          "CEThresholdSec"
+        ])
+        (assertInt "PacketLimit")
+        (assertInt "FlowLimit")
+        (assertInt "OrphanMask")
+        (assertValueOneOf "Pacing" boolValues)
+      ];
+
+      sectionTrivialLinkEqualizer = checkUnitConfig "TrivialLinkEqualizer" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "Id"
+        ])
+      ];
+
+      sectionHierarchyTokenBucket = checkUnitConfig "HierarchyTokenBucket" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "DefaultClass"
+          "RateToQuantum"
+        ])
+        (assertInt "RateToQuantum")
+      ];
+
+      sectionHierarchyTokenBucketClass = checkUnitConfig "HierarchyTokenBucketClass" [
+        (assertOnlyFields [
+          "Parent"
+          "ClassId"
+          "Priority"
+          "QuantumBytes"
+          "MTUBytes"
+          "OverheadBytes"
+          "Rate"
+          "CeilRate"
+          "BufferBytes"
+          "CeilBufferBytes"
+        ])
+      ];
+
+      sectionHeavyHitterFilter = checkUnitConfig "HeavyHitterFilter" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 0 4294967294)
+      ];
+
+      sectionQuickFairQueueing = checkUnitConfig "QuickFairQueueing" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+        ])
+      ];
+
+      sectionQuickFairQueueingClass = checkUnitConfig "QuickFairQueueingClass" [
+        (assertOnlyFields [
+          "Parent"
+          "ClassId"
+          "Weight"
+          "MaxPacketBytes"
+        ])
+        (assertInt "Weight")
+        (assertRange "Weight" 1 1023)
+      ];
+
+      sectionBridgeVLAN = checkUnitConfig "BridgeVLAN" [
+        (assertOnlyFields [
+          "VLAN"
+          "EgressUntagged"
+          "PVID"
+        ])
+        (assertInt "PVID")
+        (assertRange "PVID" 0 4094)
+      ];
     };
   };
 
@@ -1012,6 +1526,21 @@ let
 
   };
 
+
+  l2tpSessionOptions = {
+    options = {
+      l2tpSessionConfig = mkOption {
+        default = {};
+        type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionL2TPSession;
+        description = lib.mdDoc ''
+          Each attribute in this set specifies an option in the
+          `[L2TPSession]` section of the unit.  See
+          {manpage}`systemd.netdev(5)` for details.
+        '';
+      };
+    };
+  };
+
   wireguardPeerOptions = {
     options = {
       wireguardPeerConfig = mkOption {
@@ -1125,6 +1654,38 @@ let
       '';
     };
 
+    l2tpConfig = mkOption {
+      default = {};
+      example = {
+        TunnelId = 10;
+        PeerTunnelId = 12;
+        Local = "static";
+        Remote = "192.168.30.101";
+        EncapsulationType = "ip";
+      };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionL2TP;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[L2TP]` section of the unit. See
+        {manpage}`systemd.netdev(5)` for details.
+      '';
+    };
+
+    l2tpSessions = mkOption {
+      default = [];
+      example = [ { l2tpSessionConfig={
+        SessionId = 25;
+        PeerSessionId = 26;
+        Name = "l2tp-sess";
+      };}];
+      type = with types; listOf (submodule l2tpSessionOptions);
+      description = lib.mdDoc ''
+        Each item in this array specifies an option in the
+        `[L2TPSession]` section of the unit. See
+        {manpage}`systemd.netdev(5)` for details.
+      '';
+    };
+
     wireguardConfig = mkOption {
       default = {};
       example = {
@@ -1306,6 +1867,51 @@ let
     };
   };
 
+  bridgeFDBOptions = {
+    options = {
+      bridgeFDBConfig = mkOption {
+        default = {};
+        example = { MACAddress = "65:43:4a:5b:d8:5f"; Destination = "192.168.1.42"; VNI = 20; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionBridgeFDB;
+        description = lib.mdDoc ''
+          Each attribute in this set specifies an option in the
+          `[BridgeFDB]` section of the unit.  See
+          {manpage}`systemd.network(5)` for details.
+        '';
+      };
+    };
+  };
+
+  bridgeMDBOptions = {
+    options = {
+      bridgeMDBConfig = mkOption {
+        default = {};
+        example = { MulticastGroupAddress = "ff02::1:2:3:4"; VLANId = 10; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionBridgeMDB;
+        description = lib.mdDoc ''
+          Each attribute in this set specifies an option in the
+          `[BridgeMDB]` section of the unit.  See
+          {manpage}`systemd.network(5)` for details.
+        '';
+      };
+    };
+  };
+
+  bridgeVLANOptions = {
+    options = {
+      bridgeVLANConfig = mkOption {
+        default = {};
+        example = { VLAN = 20; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionBridgeVLAN;
+        description = lib.mdDoc ''
+          Each attribute in this set specifies an option in the
+          `[BridgeVLAN]` section of the unit.  See
+          {manpage}`systemd.network(5)` for details.
+        '';
+      };
+    };
+  };
+
   networkOptions = commonNetworkOptions // {
 
     linkConfig = mkOption {
@@ -1445,6 +2051,355 @@ let
       '';
     };
 
+    bridgeConfig = mkOption {
+      default = {};
+      example = { MulticastFlood = false; Cost = 20; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionBridge;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[Bridge]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    bridgeFDBs = mkOption {
+      default = [];
+      example = [ { bridgeFDBConfig = { MACAddress = "90:e2:ba:43:fc:71"; Destination = "192.168.100.4"; VNI = 3600; }; } ];
+      type = with types; listOf (submodule bridgeFDBOptions);
+      description = lib.mdDoc ''
+        A list of BridgeFDB sections to be added to the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    bridgeMDBs = mkOption {
+      default = [];
+      example = [ { bridgeMDBConfig = { MulticastGroupAddress = "ff02::1:2:3:4"; VLANId = 10; } ; } ];
+      type = with types; listOf (submodule bridgeMDBOptions);
+      description = lib.mdDoc ''
+        A list of BridgeMDB sections to be added to the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    lldpConfig = mkOption {
+      default = {};
+      example = { MUDURL = "https://things.example.org/product_abc123/v5"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionLLDP;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[LLDP]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    canConfig = mkOption {
+      default = {};
+      example = { };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionCAN;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[CAN]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    ipoIBConfig = mkOption {
+      default = {};
+      example = { };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionIPoIB;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[IPoIB]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    qdiscConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionQDisc;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[QDisc]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    networkEmulatorConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; DelaySec = "20msec"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionNetworkEmulator;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[NetworkEmulator]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    tokenBucketFilterConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; Rate = "100k"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionTokenBucketFilter;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[TokenBucketFilter]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    pieConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; PacketLimit = "3847"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionPIE;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[PIE]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    flowQueuePIEConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; PacketLimit = "3847"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionFlowQueuePIE;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[FlowQueuePIE]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    stochasticFairBlueConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; PacketLimit = "3847"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionStochasticFairBlue;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[StochasticFairBlue]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    stochasticFairnessQueueingConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; PerturbPeriodSec = "30"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionStochasticFairnessQueueing;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[StochasticFairnessQueueing]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    bfifoConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; LimitBytes = "20K"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionBFIFO;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[BFIFO]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    pfifoConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; PacketLimit = "300"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionPFIFO;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[PFIFO]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    pfifoHeadDropConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; PacketLimit = "300"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionPFIFOHeadDrop;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[PFIFOHeadDrop]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    pfifoFastConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionPFIFOFast;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[PFIFOFast]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    cakeConfig = mkOption {
+      default = {};
+      example = { Bandwidth = "40M"; OverheadBytes = 8; CompensationMode = "ptm"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionCAKE;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[CAKE]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    controlledDelayConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; TargetSec = "20msec"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionControlledDelay;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[ControlledDelay]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    deficitRoundRobinSchedulerConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionDeficitRoundRobinScheduler;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[DeficitRoundRobinScheduler]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    deficitRoundRobinSchedulerClassConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; QuantumBytes = "300k"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionDeficitRoundRobinSchedulerClass;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[DeficitRoundRobinSchedulerClass]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    enhancedTransmissionSelectionConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; QuantumBytes = "300k"; Bands = 3; PriorityMap = "100 200 300"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionEnhancedTransmissionSelection;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[EnhancedTransmissionSelection]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    genericRandomEarlyDetectionConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; VirtualQueues = 5; DefaultVirtualQueue = 3; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionGenericRandomEarlyDetection;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[GenericRandomEarlyDetection]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    fairQueueingControlledDelayConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; Flows = 5; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionFairQueueingControlledDelay;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[FairQueueingControlledDelay]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    fairQueueingConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; FlowLimit = 5; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionFairQueueing;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[FairQueueing]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    trivialLinkEqualizerConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; Id = 0; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionTrivialLinkEqualizer;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[TrivialLinkEqualizer]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    hierarchyTokenBucketConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionHierarchyTokenBucket;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[HierarchyTokenBucket]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    hierarchyTokenBucketClassConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; Rate = "10M"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionHierarchyTokenBucketClass;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[HierarchyTokenBucketClass]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    heavyHitterFilterConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; PacketLimit = 10000; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionHeavyHitterFilter;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[HeavyHitterFilter]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    quickFairQueueingConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionQuickFairQueueing;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[QuickFairQueueing]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    quickFairQueueingConfigClass = mkOption {
+      default = {};
+      example = { Parent = "root"; Weight = 133; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionQuickFairQueueingClass;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[QuickFairQueueingClass]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    bridgeVLANs = mkOption {
+      default = [];
+      example = [ { bridgeVLANConfig = { VLAN = "10-20"; }; } ];
+      type = with types; listOf (submodule bridgeVLANOptions);
+      description = lib.mdDoc ''
+        A list of BridgeVLAN sections to be added to the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
     name = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -1550,6 +2505,15 @@ let
       '';
     };
 
+    macvtap = mkOption {
+      default = [ ];
+      type = types.listOf types.str;
+      description = lib.mdDoc ''
+        A list of macvtap interfaces to be added to the network section of the
+        unit.  See {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
     vxlan = mkOption {
       default = [ ];
       type = types.listOf types.str;
@@ -1651,87 +2615,6 @@ let
     };
   };
 
-  commonMatchText = def: optionalString (def.matchConfig != { }) ''
-    [Match]
-    ${attrsToSection def.matchConfig}
-  '';
-
-  linkToUnit = name: def:
-    { inherit (def) enable;
-      text = commonMatchText def
-        + ''
-          [Link]
-          ${attrsToSection def.linkConfig}
-        ''
-        + def.extraConfig;
-    };
-
-  netdevToUnit = name: def:
-    { inherit (def) enable;
-      text = commonMatchText def
-        + ''
-          [NetDev]
-          ${attrsToSection def.netdevConfig}
-        ''
-        + optionalString (def.vlanConfig != { }) ''
-          [VLAN]
-          ${attrsToSection def.vlanConfig}
-        ''
-        + optionalString (def.macvlanConfig != { }) ''
-          [MACVLAN]
-          ${attrsToSection def.macvlanConfig}
-        ''
-        + optionalString (def.vxlanConfig != { }) ''
-          [VXLAN]
-          ${attrsToSection def.vxlanConfig}
-        ''
-        + optionalString (def.tunnelConfig != { }) ''
-          [Tunnel]
-          ${attrsToSection def.tunnelConfig}
-        ''
-        + optionalString (def.fooOverUDPConfig != { }) ''
-          [FooOverUDP]
-          ${attrsToSection def.fooOverUDPConfig}
-        ''
-        + optionalString (def.peerConfig != { }) ''
-          [Peer]
-          ${attrsToSection def.peerConfig}
-        ''
-        + optionalString (def.tunConfig != { }) ''
-          [Tun]
-          ${attrsToSection def.tunConfig}
-        ''
-        + optionalString (def.tapConfig != { }) ''
-          [Tap]
-          ${attrsToSection def.tapConfig}
-        ''
-        + optionalString (def.wireguardConfig != { }) ''
-          [WireGuard]
-          ${attrsToSection def.wireguardConfig}
-        ''
-        + flip concatMapStrings def.wireguardPeers (x: ''
-          [WireGuardPeer]
-          ${attrsToSection x.wireguardPeerConfig}
-        '')
-        + optionalString (def.bondConfig != { }) ''
-          [Bond]
-          ${attrsToSection def.bondConfig}
-        ''
-        + optionalString (def.xfrmConfig != { }) ''
-          [Xfrm]
-          ${attrsToSection def.xfrmConfig}
-        ''
-        + optionalString (def.vrfConfig != { }) ''
-          [VRF]
-          ${attrsToSection def.vrfConfig}
-        ''
-        + optionalString (def.batmanAdvancedConfig != { }) ''
-          [BatmanAdvanced]
-          ${attrsToSection def.batmanAdvancedConfig}
-        ''
-        + def.extraConfig;
-    };
-
   renderConfig = def:
     { text = ''
         [Network]
@@ -1746,117 +2629,14 @@ let
         ${attrsToSection def.dhcpV6Config}
       ''; };
 
-  networkToUnit = name: def:
-    { inherit (def) enable;
-      text = commonMatchText def
-        + optionalString (def.linkConfig != { }) ''
-          [Link]
-          ${attrsToSection def.linkConfig}
-        ''
-        + ''
-          [Network]
-        ''
-        + attrsToSection def.networkConfig
-        + optionalString (def.address != [ ]) ''
-          ${concatStringsSep "\n" (map (s: "Address=${s}") def.address)}
-        ''
-        + optionalString (def.gateway != [ ]) ''
-          ${concatStringsSep "\n" (map (s: "Gateway=${s}") def.gateway)}
-        ''
-        + optionalString (def.dns != [ ]) ''
-          ${concatStringsSep "\n" (map (s: "DNS=${s}") def.dns)}
-        ''
-        + optionalString (def.ntp != [ ]) ''
-          ${concatStringsSep "\n" (map (s: "NTP=${s}") def.ntp)}
-        ''
-        + optionalString (def.bridge != [ ]) ''
-          ${concatStringsSep "\n" (map (s: "Bridge=${s}") def.bridge)}
-        ''
-        + optionalString (def.bond != [ ]) ''
-          ${concatStringsSep "\n" (map (s: "Bond=${s}") def.bond)}
-        ''
-        + optionalString (def.vrf != [ ]) ''
-          ${concatStringsSep "\n" (map (s: "VRF=${s}") def.vrf)}
-        ''
-        + optionalString (def.vlan != [ ]) ''
-          ${concatStringsSep "\n" (map (s: "VLAN=${s}") def.vlan)}
-        ''
-        + optionalString (def.macvlan != [ ]) ''
-          ${concatStringsSep "\n" (map (s: "MACVLAN=${s}") def.macvlan)}
-        ''
-        + optionalString (def.vxlan != [ ]) ''
-          ${concatStringsSep "\n" (map (s: "VXLAN=${s}") def.vxlan)}
-        ''
-        + optionalString (def.tunnel != [ ]) ''
-          ${concatStringsSep "\n" (map (s: "Tunnel=${s}") def.tunnel)}
-        ''
-        + optionalString (def.xfrm != [ ]) ''
-          ${concatStringsSep "\n" (map (s: "Xfrm=${s}") def.xfrm)}
-        ''
-        + ''
-
-        ''
-        + flip concatMapStrings def.addresses (x: ''
-          [Address]
-          ${attrsToSection x.addressConfig}
-        '')
-        + flip concatMapStrings def.routingPolicyRules (x: ''
-          [RoutingPolicyRule]
-          ${attrsToSection x.routingPolicyRuleConfig}
-        '')
-        + flip concatMapStrings def.routes (x: ''
-          [Route]
-          ${attrsToSection x.routeConfig}
-        '')
-        + optionalString (def.dhcpV4Config != { }) ''
-          [DHCPv4]
-          ${attrsToSection def.dhcpV4Config}
-        ''
-        + optionalString (def.dhcpV6Config != { }) ''
-          [DHCPv6]
-          ${attrsToSection def.dhcpV6Config}
-        ''
-        + optionalString (def.dhcpPrefixDelegationConfig != { }) ''
-          [DHCPPrefixDelegation]
-          ${attrsToSection def.dhcpPrefixDelegationConfig}
-        ''
-        + optionalString (def.ipv6AcceptRAConfig != { }) ''
-          [IPv6AcceptRA]
-          ${attrsToSection def.ipv6AcceptRAConfig}
-        ''
-        + optionalString (def.dhcpServerConfig != { }) ''
-          [DHCPServer]
-          ${attrsToSection def.dhcpServerConfig}
-        ''
-        + optionalString (def.ipv6SendRAConfig != { }) ''
-          [IPv6SendRA]
-          ${attrsToSection def.ipv6SendRAConfig}
-        ''
-        + flip concatMapStrings def.ipv6Prefixes (x: ''
-          [IPv6Prefix]
-          ${attrsToSection x.ipv6PrefixConfig}
-        '')
-        + flip concatMapStrings def.ipv6RoutePrefixes (x: ''
-          [IPv6RoutePrefix]
-          ${attrsToSection x.ipv6RoutePrefixConfig}
-        '')
-        + flip concatMapStrings def.dhcpServerStaticLeases (x: ''
-          [DHCPServerStaticLease]
-          ${attrsToSection x.dhcpServerStaticLeaseConfig}
-        '')
-        + def.extraConfig;
-    };
-
-  unitFiles = listToAttrs (map (name: {
-    name = "systemd/network/${name}";
+  mkUnitFiles = prefix: cfg: listToAttrs (map (name: {
+    name = "${prefix}systemd/network/${name}";
     value.source = "${cfg.units.${name}.unit}/${name}";
   }) (attrNames cfg.units));
-in
 
-{
-  options = {
+  commonOptions = visible: {
 
-    systemd.network.enable = mkOption {
+    enable = mkOption {
       default = false;
       type = types.bool;
       description = lib.mdDoc ''
@@ -1864,31 +2644,35 @@ in
       '';
     };
 
-    systemd.network.links = mkOption {
+    links = mkOption {
       default = {};
+      inherit visible;
       type = with types; attrsOf (submodule [ { options = linkOptions; } ]);
       description = lib.mdDoc "Definition of systemd network links.";
     };
 
-    systemd.network.netdevs = mkOption {
+    netdevs = mkOption {
       default = {};
+      inherit visible;
       type = with types; attrsOf (submodule [ { options = netdevOptions; } ]);
       description = lib.mdDoc "Definition of systemd network devices.";
     };
 
-    systemd.network.networks = mkOption {
+    networks = mkOption {
       default = {};
+      inherit visible;
       type = with types; attrsOf (submodule [ { options = networkOptions; } networkConfig ]);
       description = lib.mdDoc "Definition of systemd networks.";
     };
 
-    systemd.network.config = mkOption {
+    config = mkOption {
       default = {};
+      inherit visible;
       type = with types; submodule [ { options = networkdOptions; } networkdConfig ];
       description = lib.mdDoc "Definition of global systemd network config.";
     };
 
-    systemd.network.units = mkOption {
+    units = mkOption {
       description = lib.mdDoc "Definition of networkd units.";
       default = {};
       internal = true;
@@ -1901,7 +2685,7 @@ in
         }));
     };
 
-    systemd.network.wait-online = {
+    wait-online = {
       enable = mkOption {
         type = types.bool;
         default = true;
@@ -1948,7 +2732,7 @@ in
           Extra command-line arguments to pass to systemd-networkd-wait-online.
           These also affect per-interface `systemd-network-wait-online@` services.
 
-          See [{manpage}`systemd-networkd-wait-online.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-networkd-wait-online.service.html) for all available options.
+          See {manpage}`systemd-networkd-wait-online.service(8)` for all available options.
         '';
         type = with types; listOf str;
         default = [];
@@ -1957,12 +2741,14 @@ in
 
   };
 
-  config = mkMerge [
+  commonConfig = config: let
+    cfg = config.systemd.network;
+    mkUnit = f: def: { inherit (def) enable; text = f def; };
+  in mkMerge [
 
     # .link units are honored by udev, no matter if systemd-networkd is enabled or not.
     {
-      systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.link" (linkToUnit n v)) cfg.links;
-      environment.etc = unitFiles;
+      systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.link" (mkUnit linkToUnit v)) cfg.links;
 
       systemd.network.wait-online.extraArgs =
         [ "--timeout=${toString cfg.wait-online.timeout}" ]
@@ -1972,30 +2758,14 @@ in
 
     (mkIf config.systemd.network.enable {
 
-      users.users.systemd-network.group = "systemd-network";
-
-      systemd.additionalUpstreamSystemUnits = [
-        "systemd-networkd-wait-online.service"
-        "systemd-networkd.service"
-        "systemd-networkd.socket"
-      ];
-
-      systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.netdev" (netdevToUnit n v)) cfg.netdevs
-        // mapAttrs' (n: v: nameValuePair "${n}.network" (networkToUnit n v)) cfg.networks;
+      systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.netdev" (mkUnit netdevToUnit v)) cfg.netdevs
+        // mapAttrs' (n: v: nameValuePair "${n}.network" (mkUnit networkToUnit v)) cfg.networks;
 
       # systemd-networkd is socket-activated by kernel netlink route change
       # messages. It is important to have systemd buffer those on behalf of
       # networkd.
       systemd.sockets.systemd-networkd.wantedBy = [ "sockets.target" ];
 
-      systemd.services.systemd-networkd = {
-        wantedBy = [ "multi-user.target" ];
-        aliases = [ "dbus-org.freedesktop.network1.service" ];
-        restartTriggers = map (x: x.source) (attrValues unitFiles) ++ [
-          config.environment.etc."systemd/networkd.conf".source
-        ];
-      };
-
       systemd.services.systemd-networkd-wait-online = {
         inherit (cfg.wait-online) enable;
         wantedBy = [ "network-online.target" ];
@@ -2017,8 +2787,37 @@ in
         };
       };
 
+    })
+  ];
+
+  stage2Config = let
+    cfg = config.systemd.network;
+    unitFiles = mkUnitFiles "" cfg;
+  in mkMerge [
+    (commonConfig config)
+
+    { environment.etc = unitFiles; }
+
+    (mkIf config.systemd.network.enable {
+
+      users.users.systemd-network.group = "systemd-network";
+
+      systemd.additionalUpstreamSystemUnits = [
+        "systemd-networkd-wait-online.service"
+        "systemd-networkd.service"
+        "systemd-networkd.socket"
+      ];
+
       environment.etc."systemd/networkd.conf" = renderConfig cfg.config;
 
+      systemd.services.systemd-networkd = {
+        wantedBy = [ "multi-user.target" ];
+        restartTriggers = map (x: x.source) (attrValues unitFiles) ++ [
+          config.environment.etc."systemd/networkd.conf".source
+        ];
+        aliases = [ "dbus-org.freedesktop.network1.service" ];
+      };
+
       networking.iproute2 = mkIf (cfg.config.addRouteTablesToIPRoute2 && cfg.config.routeTables != { }) {
         enable = mkDefault true;
         rttablesExtraConfig = ''
@@ -2029,6 +2828,129 @@ in
       };
 
       services.resolved.enable = mkDefault true;
+
+    })
+  ];
+
+  stage1Config = let
+    cfg = config.boot.initrd.systemd.network;
+  in mkMerge [
+    (commonConfig config.boot.initrd)
+
+    {
+      systemd.network.enable = mkDefault config.boot.initrd.network.enable;
+      systemd.contents = mkUnitFiles "/etc/" cfg;
+
+      # Networkd link files are used early by udev to set up interfaces early.
+      # This must be done in stage 1 to avoid race conditions between udev and
+      # network daemons.
+      systemd.network.units = lib.filterAttrs (n: _: hasSuffix ".link" n) config.systemd.network.units;
+      systemd.storePaths = ["${config.boot.initrd.systemd.package}/lib/systemd/network/99-default.link"];
+    }
+
+    (mkIf cfg.enable {
+
+      systemd.package = mkDefault pkgs.systemdStage1Network;
+
+      # For networkctl
+      systemd.dbus.enable = mkDefault true;
+
+      systemd.additionalUpstreamUnits = [
+        "systemd-networkd-wait-online.service"
+        "systemd-networkd.service"
+        "systemd-networkd.socket"
+        "systemd-network-generator.service"
+        "network-online.target"
+        "network-pre.target"
+        "network.target"
+        "nss-lookup.target"
+        "nss-user-lookup.target"
+        "remote-fs-pre.target"
+        "remote-fs.target"
+      ];
+      systemd.users.systemd-network = {};
+      systemd.groups.systemd-network = {};
+
+      systemd.contents."/etc/systemd/networkd.conf" = renderConfig cfg.config;
+
+      systemd.services.systemd-networkd = {
+        wantedBy = [ "initrd.target" ];
+        # These before and conflicts lines can be removed when this PR makes it into a release:
+        # https://github.com/systemd/systemd/pull/27791
+        before = ["initrd-switch-root.target"];
+        conflicts = ["initrd-switch-root.target"];
+      };
+      systemd.sockets.systemd-networkd = {
+        wantedBy = [ "initrd.target" ];
+        before = ["initrd-switch-root.target"];
+        conflicts = ["initrd-switch-root.target"];
+      };
+
+      systemd.services.systemd-network-generator.wantedBy = [ "sysinit.target" ];
+
+      systemd.storePaths = [
+        "${config.boot.initrd.systemd.package}/lib/systemd/systemd-networkd"
+        "${config.boot.initrd.systemd.package}/lib/systemd/systemd-networkd-wait-online"
+        "${config.boot.initrd.systemd.package}/lib/systemd/systemd-network-generator"
+      ];
+      kernelModules = [ "af_packet" ];
+
+      systemd.services.nixos-flush-networkd = mkIf config.boot.initrd.network.flushBeforeStage2 {
+        description = "Flush Network Configuration";
+        wantedBy = ["initrd.target"];
+        after = ["systemd-networkd.service" "dbus.socket" "dbus.service"];
+        before = ["shutdown.target" "initrd-switch-root.target"];
+        conflicts = ["shutdown.target" "initrd-switch-root.target"];
+        unitConfig.DefaultDependencies = false;
+        serviceConfig = {
+          # This service does nothing when starting, but brings down
+          # interfaces when switching root. This is the easiest way to
+          # ensure proper ordering while stopping. See systemd.unit(5)
+          # section on Before= and After=. The important part is that
+          # we are stopped before units we need, like dbus.service,
+          # and that we are stopped before starting units like
+          # initrd-switch-root.target
+          Type = "oneshot";
+          RemainAfterExit = true;
+          ExecStart = "/bin/true";
+        };
+        # systemd-networkd doesn't bring down interfaces on its own
+        # when it exits (see: systemd-networkd(8)), so we have to do
+        # it ourselves. The networkctl command doesn't have a way to
+        # bring all interfaces down, so we have to iterate over the
+        # list and filter out unmanaged interfaces to bring them down
+        # individually.
+        preStop = ''
+          networkctl list --full --no-legend | while read _idx link _type _operational setup _; do
+            [ "$setup" = unmanaged ] && continue
+            networkctl down "$link"
+          done
+        '';
+      };
+
+    })
+  ];
+
+in
+
+{
+  options = {
+    systemd.network = commonOptions true;
+    boot.initrd.systemd.network = commonOptions "shallow";
+  };
+
+  config = mkMerge [
+    stage2Config
+    (mkIf config.boot.initrd.systemd.enable {
+      assertions = [{
+        assertion = config.boot.initrd.network.udhcpc.extraArgs == [];
+        message = ''
+          boot.initrd.network.udhcpc.extraArgs is not supported when
+          boot.initrd.systemd.enable is enabled
+        '';
+      }];
+
+      boot.initrd = stage1Config;
     })
   ];
 }
diff --git a/nixos/modules/system/boot/plymouth.nix b/nixos/modules/system/boot/plymouth.nix
index 9b6472fea42..b041b8951fa 100644
--- a/nixos/modules/system/boot/plymouth.nix
+++ b/nixos/modules/system/boot/plymouth.nix
@@ -98,10 +98,13 @@ in
         type = types.path;
         # Dimensions are 48x48 to match GDM logo
         default = "${nixos-icons}/share/icons/hicolor/48x48/apps/nix-snowflake-white.png";
-        defaultText = literalExpression ''pkgs.fetchurl {
-          url = "https://nixos.org/logo/nixos-hires.png";
-          sha256 = "1ivzgd7iz0i06y36p8m5w48fd8pjqwxhdaavc0pxs7w1g7mcy5si";
-        }'';
+        defaultText = literalExpression ''"''${nixos-icons}/share/icons/hicolor/48x48/apps/nix-snowflake-white.png"'';
+        example = literalExpression ''
+          pkgs.fetchurl {
+            url = "https://nixos.org/logo/nixos-hires.png";
+            sha256 = "1ivzgd7iz0i06y36p8m5w48fd8pjqwxhdaavc0pxs7w1g7mcy5si";
+          }
+        '';
         description = lib.mdDoc ''
           Logo which is displayed on the splash screen.
         '';
@@ -134,6 +137,13 @@ in
     # XXX: Needed because we supply a different set of plugins in initrd.
     environment.etc."plymouth/plugins".source = "${plymouth}/lib/plymouth";
 
+    systemd.tmpfiles.rules = [
+      "d /run/plymouth 0755 root root 0 -"
+      "L+ /run/plymouth/plymouthd.defaults - - - - /etc/plymouth/plymouthd.defaults"
+      "L+ /run/plymouth/themes - - - - /etc/plymouth/themes"
+      "L+ /run/plymouth/plugins - - - - /etc/plymouth/plugins"
+    ];
+
     systemd.packages = [ plymouth ];
 
     systemd.services.plymouth-kexec.wantedBy = [ "kexec.target" ];
@@ -146,6 +156,9 @@ in
     systemd.services.systemd-ask-password-plymouth.wantedBy = [ "multi-user.target" ];
     systemd.paths.systemd-ask-password-plymouth.wantedBy = [ "multi-user.target" ];
 
+    # Prevent Plymouth taking over the screen during system updates.
+    systemd.services.plymouth-start.restartIfChanged = false;
+
     boot.initrd.systemd = {
       extraBin.plymouth = "${plymouth}/bin/plymouth"; # for the recovery shell
       storePaths = [
@@ -157,8 +170,8 @@ in
       contents = {
         # Files
         "/etc/plymouth/plymouthd.conf".source = configFile;
-        "/etc/plymouth/plymouthd.defaults".source = "${plymouth}/share/plymouth/plymouthd.defaults";
         "/etc/plymouth/logo.png".source = cfg.logo;
+        "/etc/plymouth/plymouthd.defaults".source = "${plymouth}/share/plymouth/plymouthd.defaults";
         # Directories
         "/etc/plymouth/plugins".source = pkgs.runCommand "plymouth-initrd-plugins" {} ''
           # Check if the actual requested theme is here
@@ -171,8 +184,8 @@ in
 
           mkdir -p $out/renderers
           # module might come from a theme
-          cp ${themesEnv}/lib/plymouth/{text,details,label,$moduleName}.so $out
-          cp ${plymouth}/lib/plymouth/renderers/{drm,frame-buffer}.so $out/renderers
+          cp ${themesEnv}/lib/plymouth/*.so $out
+          cp ${plymouth}/lib/plymouth/renderers/*.so $out/renderers
         '';
         "/etc/plymouth/themes".source = pkgs.runCommand "plymouth-initrd-themes" {} ''
           # Check if the actual requested theme is here
@@ -181,19 +194,24 @@ in
               exit 1
           fi
 
-          mkdir $out
-          cp -r ${themesEnv}/share/plymouth/themes/${cfg.theme} $out
+          mkdir -p $out/${cfg.theme}
+          cp -r ${themesEnv}/share/plymouth/themes/${cfg.theme}/* $out/${cfg.theme}
           # Copy more themes if the theme depends on others
-          for theme in $(grep -hRo '/etc/plymouth/themes/.*$' $out | xargs -n1 basename); do
+          for theme in $(grep -hRo '/share/plymouth/themes/.*$' $out | xargs -n1 basename); do
               if [[ -d "${themesEnv}/share/plymouth/themes/$theme" ]]; then
                   if [[ ! -d "$out/$theme" ]]; then
                     echo "Adding dependent theme: $theme"
-                    cp -r "${themesEnv}/share/plymouth/themes/$theme" $out
+                    mkdir -p "$out/$theme"
+                    cp -r "${themesEnv}/share/plymouth/themes/$theme"/* "$out/$theme"
                   fi
               else
                 echo "Missing theme dependency: $theme"
               fi
           done
+          # Fixup references
+          for theme in $out/*/*.plymouth; do
+            sed -i "s,${builtins.storeDir}/.*/share/plymouth/themes,$out," "$theme"
+          done
         '';
 
         # Fonts
@@ -222,6 +240,11 @@ in
         plymouth-switch-root-initramfs.wantedBy = [ "halt.target" "kexec.target" "plymouth-switch-root-initramfs.service" "poweroff.target" "reboot.target" ];
         plymouth-switch-root.wantedBy = [ "initrd-switch-root.target" ];
       };
+      # Link in runtime files before starting
+      services.plymouth-start.preStart = ''
+        mkdir -p /run/plymouth
+        ln -sf /etc/plymouth/{plymouthd.defaults,themes,plugins} /run/plymouth/
+      '';
     };
 
     # Insert required udev rules. We take stage 2 systemd because the udev
@@ -246,8 +269,8 @@ in
 
       mkdir -p $out/lib/plymouth/renderers
       # module might come from a theme
-      cp ${themesEnv}/lib/plymouth/{text,details,label,$moduleName}.so $out/lib/plymouth
-      cp ${plymouth}/lib/plymouth/renderers/{drm,frame-buffer}.so $out/lib/plymouth/renderers
+      cp ${themesEnv}/lib/plymouth/*.so $out/lib/plymouth
+      cp ${plymouth}/lib/plymouth/renderers/*.so $out/lib/plymouth/renderers
 
       mkdir -p $out/share/plymouth/themes
       cp ${plymouth}/share/plymouth/plymouthd.defaults $out/share/plymouth
@@ -264,7 +287,7 @@ in
       chmod -R +w themes
       find themes -type f | while read file
       do
-        sed -i "s,/nix/.*/share/plymouth/themes,$out/share/plymouth/themes,g" $file
+        sed -i "s,${builtins.storeDir}/.*/share/plymouth/themes,$out/share/plymouth/themes,g" $file
       done
 
       # Install themes
@@ -272,7 +295,7 @@ in
 
       # Install logo
       mkdir -p $out/etc/plymouth
-      cp -r -L ${themesEnv}/etc/plymouth $out
+      cp -r -L ${themesEnv}/etc/plymouth $out/etc
 
       # Setup font
       mkdir -p $out/share/fonts
@@ -301,11 +324,11 @@ in
     boot.initrd.preLVMCommands = mkIf (!config.boot.initrd.systemd.enable) (mkAfter ''
       mkdir -p /etc/plymouth
       mkdir -p /run/plymouth
+      ln -s $extraUtils/etc/plymouth/logo.png /etc/plymouth/logo.png
       ln -s ${configFile} /etc/plymouth/plymouthd.conf
-      ln -s $extraUtils/share/plymouth/plymouthd.defaults /etc/plymouth/plymouthd.defaults
-      ln -s $extraUtils/share/plymouth/logo.png /etc/plymouth/logo.png
-      ln -s $extraUtils/share/plymouth/themes /etc/plymouth/themes
-      ln -s $extraUtils/lib/plymouth /etc/plymouth/plugins
+      ln -s $extraUtils/share/plymouth/plymouthd.defaults /run/plymouth/plymouthd.defaults
+      ln -s $extraUtils/share/plymouth/themes /run/plymouth/themes
+      ln -s $extraUtils/lib/plymouth /run/plymouth/plugins
       ln -s $extraUtils/etc/fonts /etc/fonts
 
       plymouthd --mode=boot --pid-file=/run/plymouth/pid --attach-to-session
diff --git a/nixos/modules/system/boot/resolved.nix b/nixos/modules/system/boot/resolved.nix
index 0ab2a875975..4e7201833db 100644
--- a/nixos/modules/system/boot/resolved.nix
+++ b/nixos/modules/system/boot/resolved.nix
@@ -16,7 +16,9 @@ in
       default = false;
       type = types.bool;
       description = lib.mdDoc ''
-        Whether to enable the systemd DNS resolver daemon.
+        Whether to enable the systemd DNS resolver daemon, `systemd-resolved`.
+
+        Search for `services.resolved` to see all options.
       '';
     };
 
diff --git a/nixos/modules/system/boot/stage-1-init.sh b/nixos/modules/system/boot/stage-1-init.sh
index 4596c160a95..bc2fc7f7b10 100644
--- a/nixos/modules/system/boot/stage-1-init.sh
+++ b/nixos/modules/system/boot/stage-1-init.sh
@@ -73,7 +73,7 @@ trap 'fail' 0
 
 # Print a greeting.
 info
-info "<<< NixOS Stage 1 >>>"
+info "<<< @distroName@ Stage 1 >>>"
 info
 
 # Make several required directories.
@@ -114,6 +114,28 @@ waitDevice() {
     done
 }
 
+# Create the mount point if required.
+makeMountPoint() {
+    local device="$1"
+    local mountPoint="$2"
+    local options="$3"
+
+    local IFS=,
+
+    # If we're bind mounting a file, the mount point should also be a file.
+    if ! [ -d "$device" ]; then
+        for opt in $options; do
+            if [ "$opt" = bind ] || [ "$opt" = rbind ]; then
+                mkdir -p "$(dirname "/mnt-root$mountPoint")"
+                touch "/mnt-root$mountPoint"
+                return
+            fi
+        done
+    fi
+
+    mkdir -m 0755 -p "/mnt-root$mountPoint"
+}
+
 # Mount special file systems.
 specialMount() {
   local device="$1"
@@ -234,8 +256,7 @@ done
 mkdir -p /lib
 ln -s @modulesClosure@/lib/modules /lib/modules
 ln -s @modulesClosure@/lib/firmware /lib/firmware
-# see comment in stage-1.nix for explanation
-echo @extraUtils@/bin/modprobe-kernel > /proc/sys/kernel/modprobe
+echo @extraUtils@/bin/modprobe > /proc/sys/kernel/modprobe
 for i in @kernelModules@; do
     info "loading module $(basename $i)..."
     modprobe $i
@@ -294,6 +315,9 @@ checkFS() {
     # Skip fsck for inherently readonly filesystems.
     if [ "$fsType" = squashfs ]; then return 0; fi
 
+    # Skip fsck.erofs because it is still experimental.
+    if [ "$fsType" = erofs ]; then return 0; fi
+
     # If we couldn't figure out the FS type, then skip fsck.
     if [ "$fsType" = auto ]; then
         echo 'cannot check filesystem with type "auto"!'
@@ -372,22 +396,6 @@ mountFS() {
 
     checkFS "$device" "$fsType"
 
-    # Optionally resize the filesystem.
-    case $options in
-        *x-nixos.autoresize*)
-            if [ "$fsType" = ext2 -o "$fsType" = ext3 -o "$fsType" = ext4 ]; then
-                modprobe "$fsType"
-                echo "resizing $device..."
-                e2fsck -fp "$device"
-                resize2fs "$device"
-            elif [ "$fsType" = f2fs ]; then
-                echo "resizing $device..."
-                fsck.f2fs -fp "$device"
-                resize.f2fs "$device"
-            fi
-            ;;
-    esac
-
     # Create backing directories for overlayfs
     if [ "$fsType" = overlay ]; then
         for i in upper work; do
@@ -398,7 +406,7 @@ mountFS() {
 
     info "mounting $device on $mountPoint..."
 
-    mkdir -p "/mnt-root$mountPoint"
+    makeMountPoint "$device" "$mountPoint" "$optionsPrefixed"
 
     # For ZFS and CIFS mounts, retry a few times before giving up.
     # We do this for ZFS as a workaround for issue NixOS/nixpkgs#25383.
@@ -411,6 +419,11 @@ mountFS() {
         n=$((n + 1))
     done
 
+    # For bind mounts, busybox has a tendency to ignore options, which can be a
+    # security issue (e.g. "nosuid"). Remounting the partition seems to fix the
+    # issue.
+    mount "/mnt-root$mountPoint" -o "remount,$optionsPrefixed"
+
     [ "$mountPoint" == "/" ] &&
         [ -f "/mnt-root/etc/NIXOS_LUSTRATE" ] &&
         lustrateRoot "/mnt-root"
@@ -422,7 +435,7 @@ lustrateRoot () {
     local root="$1"
 
     echo
-    echo -e "\e[1;33m<<< NixOS is now lustrating the root filesystem (cruft goes to /old-root) >>>\e[0m"
+    echo -e "\e[1;33m<<< @distroName@ is now lustrating the root filesystem (cruft goes to /old-root) >>>\e[0m"
     echo
 
     mkdir -m 0755 -p "$root/old-root.tmp"
@@ -438,7 +451,7 @@ lustrateRoot () {
         mv -v "$d" "$root/old-root.tmp"
     done
 
-    # Use .tmp to make sure subsequent invokations don't clash
+    # Use .tmp to make sure subsequent invocations don't clash
     mv -v "$root/old-root.tmp" "$root/old-root"
 
     mkdir -m 0755 -p "$root/etc"
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
index 95dcdfd7fbe..dcb15cf7d42 100644
--- a/nixos/modules/system/boot/stage-1.nix
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -90,7 +90,7 @@ let
   # copy what we need.  Instead of using statically linked binaries,
   # we just copy what we need from Glibc and use patchelf to make it
   # work.
-  extraUtils = pkgs.runCommandCC "extra-utils"
+  extraUtils = pkgs.runCommand "extra-utils"
     { nativeBuildInputs = [pkgs.buildPackages.nukeReferences];
       allowedReferences = [ "out" ]; # prevent accidents like glibc being included in the initrd
     }
@@ -150,32 +150,6 @@ let
       copy_bin_and_libs ${pkgs.kmod}/bin/kmod
       ln -sf kmod $out/bin/modprobe
 
-      # Dirty hack to make sure the kernel properly loads modules
-      # such as ext4 on demand (e.g. on a `mount(2)` syscall). This is necessary
-      # because `kmod` isn't linked against `libpthread.so.0` anymore (since
-      # it was merged into `libc.so.6` since version `2.34`), but still needs
-      # to access it for some reason. This is not an issue in stage-1 itself
-      # because of the `LD_LIBRARY_PATH`-variable and anytime later because the rpath of
-      # kmod/modprobe points to glibc's `$out/lib` where `libpthread.so.6` exists.
-      # However, this is a problem when the kernel calls `modprobe` inside
-      # the initial ramdisk because it doesn't know about the
-      # `LD_LIBRARY_PATH` and the rpath was nuked.
-      #
-      # Also, we can't use `makeWrapper` here because `kmod` only does
-      # `modprobe` functionality if `argv[0] == "modprobe"`.
-      cat >$out/bin/modprobe-kernel <<EOF
-      #!$out/bin/ash
-      export LD_LIBRARY_PATH=$out/lib
-      exec $out/bin/modprobe "\$@"
-      EOF
-      chmod +x $out/bin/modprobe-kernel
-
-      # Copy resize2fs if any ext* filesystems are to be resized
-      ${optionalString (any (fs: fs.autoResize && (lib.hasPrefix "ext" fs.fsType)) fileSystems) ''
-        # We need mke2fs in the initrd.
-        copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/resize2fs
-      ''}
-
       # Copy multipath.
       ${optionalString config.services.multipath.enable ''
         copy_bin_and_libs ${config.services.multipath.package}/bin/multipath
@@ -342,6 +316,8 @@ let
 
     inherit (config.boot) resumeDevice;
 
+    inherit (config.system.nixos) distroName;
+
     inherit (config.system.build) earlyMountScript;
 
     inherit (config.boot.initrd) checkJournalingFS verbose
@@ -463,7 +439,8 @@ let
           ) config.boot.initrd.secrets)
          }
 
-        (cd "$tmp" && find . -print0 | sort -z | bsdtar --uid 0 --gid 0 -cnf - -T - | bsdtar --null -cf - --format=newc @-) | \
+        # mindepth 1 so that we don't change the mode of /
+        (cd "$tmp" && find . -mindepth 1 -print0 | sort -z | bsdtar --uid 0 --gid 0 -cnf - -T - | bsdtar --null -cf - --format=newc @-) | \
           ${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1"
       '';
 
diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh
index 78cc8e8d45a..f9a2084ea9e 100755
--- a/nixos/modules/system/boot/stage-2-init.sh
+++ b/nixos/modules/system/boot/stage-2-init.sh
@@ -19,7 +19,7 @@ if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then
 
     # Print a greeting.
     echo
-    echo -e "\e[1;32m<<< NixOS Stage 2 >>>\e[0m"
+    echo -e "\e[1;32m<<< @distroName@ Stage 2 >>>\e[0m"
     echo
 
 
diff --git a/nixos/modules/system/boot/stage-2.nix b/nixos/modules/system/boot/stage-2.nix
index 6ed915c339e..001380158d5 100644
--- a/nixos/modules/system/boot/stage-2.nix
+++ b/nixos/modules/system/boot/stage-2.nix
@@ -11,6 +11,7 @@ let
     shellDebug = "${pkgs.bashInteractive}/bin/bash";
     shell = "${pkgs.bash}/bin/bash";
     inherit (config.boot) readOnlyNixStore systemdExecutable extraSystemdUnitPaths;
+    inherit (config.system.nixos) distroName;
     isExecutable = true;
     inherit useHostResolvConf;
     inherit (config.system.build) earlyMountScript;
diff --git a/nixos/modules/system/boot/stratisroot.nix b/nixos/modules/system/boot/stratisroot.nix
new file mode 100644
index 00000000000..241d044db2f
--- /dev/null
+++ b/nixos/modules/system/boot/stratisroot.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, utils, ... }:
+let
+  requiredStratisFilesystems = lib.attrsets.filterAttrs (_: x: utils.fsNeededForBoot x && x.stratis.poolUuid != null) config.fileSystems;
+in
+{
+  options = {};
+  config = lib.mkIf (requiredStratisFilesystems != {}) {
+    assertions = [
+      {
+        assertion = config.boot.initrd.systemd.enable;
+        message = "stratis root fs requires systemd stage 1";
+      }
+    ];
+    boot.initrd = {
+      systemd = {
+        storePaths = [
+          "${pkgs.stratisd}/lib/udev/stratis-base32-decode"
+          "${pkgs.stratisd}/lib/udev/stratis-str-cmp"
+          "${pkgs.lvm2.bin}/bin/dmsetup"
+          "${pkgs.stratisd}/libexec/stratisd-min"
+          "${pkgs.stratisd.initrd}/bin/stratis-rootfs-setup"
+        ];
+        packages = [pkgs.stratisd.initrd];
+        extraBin = {
+          thin_check = "${pkgs."thin-provisioning-tools"}/bin/thin_check";
+          thin_repair = "${pkgs."thin-provisioning-tools"}/bin/thin_repair";
+          thin_metadata_size = "${pkgs."thin-provisioning-tools"}/bin/thin_metadata_size";
+          stratis-min = "${pkgs.stratisd}/bin/stratis-min";
+        };
+        services =
+          lib.attrsets.mapAttrs' (
+            mountPoint: fileSystem: {
+              name = "stratis-setup-${fileSystem.stratis.poolUuid}";
+              value = {
+                description = "setup for Stratis root filesystem";
+                unitConfig.DefaultDependencies = "no";
+                conflicts = [ "shutdown.target" "initrd-switch-root.target" ];
+                onFailure = [ "emergency.target" ];
+                unitConfig.OnFailureJobMode = "isolate";
+                wants = [ "stratisd-min.service" "plymouth-start.service" ];
+                wantedBy = [ "initrd.target" ];
+                after = [ "paths.target" "plymouth-start.service" "stratisd-min.service" ];
+                before = [ "initrd.target" "shutdown.target" "initrd-switch-root.target" ];
+                environment.STRATIS_ROOTFS_UUID = fileSystem.stratis.poolUuid;
+                serviceConfig = {
+                  Type = "oneshot";
+                  ExecStart = "${pkgs.stratisd.initrd}/bin/stratis-rootfs-setup";
+                  RemainAfterExit = "yes";
+                };
+              };
+            }
+          ) requiredStratisFilesystems;
+      };
+      availableKernelModules = [ "dm-thin-pool" "dm-crypt" ] ++ [ "aes" "aes_generic" "blowfish" "twofish"
+        "serpent" "cbc" "xts" "lrw" "sha1" "sha256" "sha512"
+        "af_alg" "algif_skcipher"
+      ];
+      services.udev.packages = [
+        pkgs.stratisd.initrd
+        pkgs.lvm2
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index e37ed853181..9557cf45d56 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -79,6 +79,8 @@ let
       # Filesystems.
       "systemd-fsck@.service"
       "systemd-fsck-root.service"
+      "systemd-growfs@.service"
+      "systemd-growfs-root.service"
       "systemd-remount-fs.service"
       "systemd-pstore.service"
       "local-fs.target"
@@ -450,7 +452,7 @@ in
         (mkAfter [ "systemd" ])
       ]);
       group = (mkMerge [
-        (mkAfter [ "systemd" ])
+        (mkAfter [ "[success=merge] systemd" ]) # need merge so that NSS won't stop at file-based groups
       ]);
     };
 
@@ -585,7 +587,16 @@ in
     # Some overrides to upstream units.
     systemd.services."systemd-backlight@".restartIfChanged = false;
     systemd.services."systemd-fsck@".restartIfChanged = false;
-    systemd.services."systemd-fsck@".path = [ config.system.path ];
+    systemd.services."systemd-fsck@".path = [ pkgs.util-linux ] ++ config.system.fsPackages;
+    systemd.services."systemd-makefs@" = {
+      restartIfChanged = false;
+      path = [ pkgs.util-linux ] ++ config.system.fsPackages;
+      # Since there is no /etc/systemd/system/systemd-makefs@.service
+      # file, the units generated in /run/systemd/generator would
+      # override anything we put here. But by forcing the use of a
+      # drop-in in /etc, it does apply.
+      overrideStrategy = "asDropin";
+    };
     systemd.services.systemd-random-seed.restartIfChanged = false;
     systemd.services.systemd-remount-fs.restartIfChanged = false;
     systemd.services.systemd-update-utmp.restartIfChanged = false;
@@ -614,7 +625,7 @@ in
 
     # Avoid potentially degraded system state due to
     # "Userspace Out-Of-Memory (OOM) Killer was skipped because of a failed condition check (ConditionControlGroupController=v2)."
-    systemd.services.systemd-oomd.enable = mkIf (!cfg.enableUnifiedCgroupHierarchy) false;
+    systemd.oomd.enable = mkIf (!cfg.enableUnifiedCgroupHierarchy) false;
 
     services.logrotate.settings = {
       "/var/log/btmp" = mapAttrs (_: mkDefault) {
diff --git a/nixos/modules/system/boot/systemd/coredump.nix b/nixos/modules/system/boot/systemd/coredump.nix
index c2ca973d380..03ef00e5683 100644
--- a/nixos/modules/system/boot/systemd/coredump.nix
+++ b/nixos/modules/system/boot/systemd/coredump.nix
@@ -44,7 +44,21 @@ in {
         '';
 
         # install provided sysctl snippets
-        "sysctl.d/50-coredump.conf".source = "${systemd}/example/sysctl.d/50-coredump.conf";
+        "sysctl.d/50-coredump.conf".source =
+          # Fix systemd-coredump error caused by truncation of `kernel.core_pattern`
+          # when the `systemd` derivation name is too long. This works by substituting
+          # the path to `systemd` with a symlink that has a constant-length path.
+          #
+          # See: https://github.com/NixOS/nixpkgs/issues/213408
+          pkgs.substitute {
+            src = "${systemd}/example/sysctl.d/50-coredump.conf";
+            replacements = [
+              "--replace"
+              "${systemd}"
+              "${pkgs.symlinkJoin { name = "systemd"; paths = [ systemd ]; }}"
+            ];
+          };
+
         "sysctl.d/50-default.conf".source = "${systemd}/example/sysctl.d/50-default.conf";
       };
 
diff --git a/nixos/modules/system/boot/systemd/homed.nix b/nixos/modules/system/boot/systemd/homed.nix
new file mode 100644
index 00000000000..403d1690124
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/homed.nix
@@ -0,0 +1,43 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.homed;
+in
+{
+  options.services.homed.enable = lib.mkEnableOption (lib.mdDoc ''
+    Enable systemd home area/user account manager
+  '');
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = config.services.nscd.enable;
+        message = "systemd-homed requires the use of systemd nss module. services.nscd.enable must be set to true,";
+      }
+    ];
+
+    systemd.additionalUpstreamSystemUnits = [
+      "systemd-homed.service"
+      "systemd-homed-activate.service"
+    ];
+
+    # This is mentioned in homed's [Install] section.
+    #
+    # While homed appears to work without it, it's probably better
+    # to follow upstream recommendations.
+    services.userdbd.enable = lib.mkDefault true;
+
+    systemd.services = {
+      systemd-homed = {
+        # These packages are required to manage encrypted volumes
+        path = config.system.fsPackages;
+        aliases = [ "dbus-org.freedesktop.home1.service" ];
+        wantedBy = [ "multi-user.target" ];
+      };
+
+      systemd-homed-activate = {
+        wantedBy = [ "systemd-homed.service" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/system/boot/systemd/initrd-secrets.nix b/nixos/modules/system/boot/systemd/initrd-secrets.nix
index bc65880719d..7b59c0cbe7b 100644
--- a/nixos/modules/system/boot/systemd/initrd-secrets.nix
+++ b/nixos/modules/system/boot/systemd/initrd-secrets.nix
@@ -19,13 +19,13 @@
       # drop this service, we'd mount the /run tmpfs over the secret, making it
       # invisible in stage 2.
       script = ''
-        for secret in $(cd /.initrd-secrets; find . -type f); do
+        for secret in $(cd /.initrd-secrets; find . -type f -o -type l); do
           mkdir -p "$(dirname "/$secret")"
           cp "/.initrd-secrets/$secret" "/$secret"
         done
       '';
 
-      unitConfig = {
+      serviceConfig = {
         Type = "oneshot";
         RemainAfterExit = true;
       };
diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix
index 196f44ccd78..3f40a5b2dfa 100644
--- a/nixos/modules/system/boot/systemd/initrd.nix
+++ b/nixos/modules/system/boot/systemd/initrd.nix
@@ -1,4 +1,4 @@
-{ lib, config, utils, pkgs, ... }:
+{ lib, options, config, utils, pkgs, ... }:
 
 with lib;
 
@@ -71,15 +71,6 @@ let
     "systemd-tmpfiles-setup.service"
     "timers.target"
     "umount.target"
-
-    # TODO: Networking
-    # "network-online.target"
-    # "network-pre.target"
-    # "network.target"
-    # "nss-lookup.target"
-    # "nss-user-lookup.target"
-    # "remote-fs-pre.target"
-    # "remote-fs.target"
   ] ++ cfg.additionalUpstreamUnits;
 
   upstreamWants = [
@@ -101,7 +92,6 @@ let
   fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems;
 
   needMakefs = lib.any (fs: fs.autoFormat) fileSystems;
-  needGrowfs = lib.any (fs: fs.autoResize) fileSystems;
 
   kernel-name = config.boot.kernelPackages.kernel.name or "kernel";
   modulesTree = config.system.modulesTree.override { name = kernel-name + "-modules"; };
@@ -118,7 +108,7 @@ let
     name = "initrd-bin-env";
     paths = map getBin cfg.initrdBin;
     pathsToLink = ["/bin" "/sbin"];
-    postBuild = concatStringsSep "\n" (mapAttrsToList (n: v: "ln -s '${v}' $out/bin/'${n}'") cfg.extraBin);
+    postBuild = concatStringsSep "\n" (mapAttrsToList (n: v: "ln -sf '${v}' $out/bin/'${n}'") cfg.extraBin);
   };
 
   initialRamdisk = pkgs.makeInitrdNG {
@@ -134,18 +124,20 @@ in {
   options.boot.initrd.systemd = {
     enable = mkEnableOption (lib.mdDoc "systemd in initrd") // {
       description = lib.mdDoc ''
-        Whether to enable systemd in initrd.
-
-        Note: This is in very early development and is highly
-        experimental. Most of the features NixOS supports in initrd are
-        not yet supported by the intrd generated with this option.
+        Whether to enable systemd in initrd. The unit options such as
+        {option}`boot.initrd.systemd.services` are the same as their
+        stage 2 counterparts such as {option}`systemd.services`,
+        except that `restartTriggers` and `reloadTriggers` are not
+        supported.
+
+        Note: This is experimental. Some of the `boot.initrd` options
+        are not supported when this is enabled, and the options under
+        `boot.initrd.systemd` are subject to change.
       '';
     };
 
-    package = (mkPackageOption pkgs "systemd" {
+    package = mkPackageOptionMD pkgs "systemd" {
       default = "systemdStage1";
-    }) // {
-      visible = false;
     };
 
     extraConfig = mkOption {
@@ -158,6 +150,16 @@ in {
       '';
     };
 
+    managerEnvironment = mkOption {
+      type = with types; attrsOf (nullOr (oneOf [ str path package ]));
+      default = {};
+      example = { SYSTEMD_LOG_LEVEL = "debug"; };
+      description = lib.mdDoc ''
+        Environment variables of PID 1. These variables are
+        *not* passed to started units.
+      '';
+    };
+
     contents = mkOption {
       description = lib.mdDoc "Set of files that have to be linked into the initrd";
       example = literalExpression ''
@@ -165,7 +167,6 @@ in {
           "/etc/hostname".text = "mymachine";
         }
       '';
-      visible = false;
       default = {};
       type = utils.systemdUtils.types.initrdContents;
     };
@@ -215,7 +216,6 @@ in {
 
     emergencyAccess = mkOption {
       type = with types; oneOf [ bool (nullOr (passwdEntry str)) ];
-      visible = false;
       description = lib.mdDoc ''
         Set to true for unauthenticated emergency access, and false for
         no emergency access.
@@ -229,7 +229,6 @@ in {
     initrdBin = mkOption {
       type = types.listOf types.package;
       default = [];
-      visible = false;
       description = lib.mdDoc ''
         Packages to include in /bin for the stage 1 emergency shell.
       '';
@@ -238,7 +237,6 @@ in {
     additionalUpstreamUnits = mkOption {
       default = [ ];
       type = types.listOf types.str;
-      visible = false;
       example = [ "debug-shell.service" "systemd-quotacheck.service" ];
       description = lib.mdDoc ''
         Additional units shipped with systemd that shall be enabled.
@@ -249,7 +247,6 @@ in {
       default = [ ];
       type = types.listOf types.str;
       example = [ "systemd-backlight@.service" ];
-      visible = false;
       description = lib.mdDoc ''
         A list of units to skip when generating system systemd configuration directory. This has
         priority over upstream units, {option}`boot.initrd.systemd.units`, and
@@ -262,13 +259,12 @@ in {
     units = mkOption {
       description = lib.mdDoc "Definition of systemd units.";
       default = {};
-      visible = false;
+      visible = "shallow";
       type = systemdUtils.types.units;
     };
 
     packages = mkOption {
       default = [];
-      visible = false;
       type = types.listOf types.package;
       example = literalExpression "[ pkgs.systemd-cryptsetup-generator ]";
       description = lib.mdDoc "Packages providing systemd units and hooks.";
@@ -276,7 +272,7 @@ in {
 
     targets = mkOption {
       default = {};
-      visible = false;
+      visible = "shallow";
       type = systemdUtils.types.initrdTargets;
       description = lib.mdDoc "Definition of systemd target units.";
     };
@@ -284,35 +280,35 @@ in {
     services = mkOption {
       default = {};
       type = systemdUtils.types.initrdServices;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc "Definition of systemd service units.";
     };
 
     sockets = mkOption {
       default = {};
       type = systemdUtils.types.initrdSockets;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc "Definition of systemd socket units.";
     };
 
     timers = mkOption {
       default = {};
       type = systemdUtils.types.initrdTimers;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc "Definition of systemd timer units.";
     };
 
     paths = mkOption {
       default = {};
       type = systemdUtils.types.initrdPaths;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc "Definition of systemd path units.";
     };
 
     mounts = mkOption {
       default = [];
       type = systemdUtils.types.initrdMounts;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc ''
         Definition of systemd mount units.
         This is a list instead of an attrSet, because systemd mandates the names to be derived from
@@ -323,7 +319,7 @@ in {
     automounts = mkOption {
       default = [];
       type = systemdUtils.types.automounts;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc ''
         Definition of systemd automount units.
         This is a list instead of an attrSet, because systemd mandates the names to be derived from
@@ -334,7 +330,7 @@ in {
     slices = mkOption {
       default = {};
       type = systemdUtils.types.slices;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc "Definition of slice configurations.";
     };
   };
@@ -343,9 +339,11 @@ in {
     system.build = { inherit initialRamdisk; };
 
     boot.initrd.availableKernelModules = [
-      "autofs4"           # systemd needs this for some features
-      "tpm-tis" "tpm-crb" # systemd-cryptenroll
-    ];
+      # systemd needs this for some features
+      "autofs4"
+      # systemd-cryptenroll
+      "tpm-tis"
+    ] ++ lib.optional (pkgs.stdenv.hostPlatform.system != "riscv64-linux") "tpm-crb";
 
     boot.initrd.systemd = {
       initrdBin = [pkgs.bash pkgs.coreutils cfg.package.kmod cfg.package] ++ config.system.fsPackages;
@@ -353,16 +351,21 @@ in {
         less = "${pkgs.less}/bin/less";
         mount = "${cfg.package.util-linux}/bin/mount";
         umount = "${cfg.package.util-linux}/bin/umount";
+        fsck = "${cfg.package.util-linux}/bin/fsck";
       };
 
+      managerEnvironment.PATH = "/bin:/sbin";
+
       contents = {
+        "/tmp/.keep".text = "systemd requires the /tmp mount point in the initrd cpio archive";
         "/init".source = "${cfg.package}/lib/systemd/systemd";
         "/etc/systemd/system".source = stage1Units;
 
         "/etc/systemd/system.conf".text = ''
           [Manager]
-          DefaultEnvironment=PATH=/bin:/sbin ${optionalString (isBool cfg.emergencyAccess && cfg.emergencyAccess) "SYSTEMD_SULOGIN_FORCE=1"}
+          DefaultEnvironment=PATH=/bin:/sbin
           ${cfg.extraConfig}
+          ManagerEnvironment=${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "${n}=${lib.escapeShellArg v}") cfg.managerEnvironment)}
         '';
 
         "/lib/modules".source = "${modulesClosure}/lib/modules";
@@ -370,8 +373,10 @@ in {
 
         "/etc/modules-load.d/nixos.conf".text = concatStringsSep "\n" config.boot.initrd.kernelModules;
 
-        "/etc/passwd".source = "${pkgs.fakeNss}/etc/passwd";
-        "/etc/shadow".text = "root:${if isBool cfg.emergencyAccess then "!" else cfg.emergencyAccess}:::::::";
+        # We can use either ! or * to lock the root account in the
+        # console, but some software like OpenSSH won't even allow you
+        # to log in with an SSH key if you use ! so we use * instead
+        "/etc/shadow".text = "root:${if isBool cfg.emergencyAccess then optionalString (!cfg.emergencyAccess) "*" else cfg.emergencyAccess}:::::::";
 
         "/bin".source = "${initrdBinEnv}/bin";
         "/sbin".source = "${initrdBinEnv}/sbin";
@@ -393,7 +398,6 @@ in {
       storePaths = [
         # systemd tooling
         "${cfg.package}/lib/systemd/systemd-fsck"
-        (lib.mkIf needGrowfs "${cfg.package}/lib/systemd/systemd-growfs")
         "${cfg.package}/lib/systemd/systemd-hibernate-resume"
         "${cfg.package}/lib/systemd/systemd-journald"
         (lib.mkIf needMakefs "${cfg.package}/lib/systemd/systemd-makefs")
@@ -425,9 +429,6 @@ in {
         # fido2 support
         "${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-fido2.so"
         "${pkgs.libfido2}/lib/libfido2.so.1"
-
-        # the unwrapped systemd-cryptsetup executable
-        "${cfg.package}/lib/systemd/.systemd-cryptsetup-wrapped"
       ] ++ jobScripts;
 
       targets.initrd.aliases = ["default.target"];
@@ -445,21 +446,6 @@ in {
                      (v: let n = escapeSystemdPath v.where;
                          in nameValuePair "${n}.automount" (automountToUnit n v)) cfg.automounts);
 
-      # The unit in /run/systemd/generator shadows the unit in
-      # /etc/systemd/system, but will still apply drop-ins from
-      # /etc/systemd/system/foo.service.d/
-      #
-      # We need IgnoreOnIsolate, otherwise the Requires dependency of
-      # a mount unit on its makefs unit causes it to be unmounted when
-      # we isolate for switch-root. Use a dummy package so that
-      # generateUnits will generate drop-ins instead of unit files.
-      packages = [(pkgs.runCommand "dummy" {} ''
-        mkdir -p $out/etc/systemd/system
-        touch $out/etc/systemd/system/systemd-{makefs,growfs}@.service
-      '')];
-      services."systemd-makefs@" = lib.mkIf needMakefs { unitConfig.IgnoreOnIsolate = true; };
-      services."systemd-growfs@" = lib.mkIf needGrowfs { unitConfig.IgnoreOnIsolate = true; };
-
       # make sure all the /dev nodes are set up
       services.systemd-tmpfiles-setup-dev.wantedBy = ["sysinit.target"];
 
@@ -493,7 +479,7 @@ in {
 
           # If we are not booting a NixOS closure (e.g. init=/bin/sh),
           # we don't know what root to prepare so we don't do anything
-          if ! [ -x "/sysroot$closure/prepare-root" ]; then
+          if ! [ -x "/sysroot$(readlink "/sysroot$closure/prepare-root" || echo "$closure/prepare-root")" ]; then
             echo "NEW_INIT=''${initParam[1]}" > /etc/switch-root.conf
             echo "$closure does not look like a NixOS installation - not activating"
             exit 0
diff --git a/nixos/modules/system/boot/systemd/logind.nix b/nixos/modules/system/boot/systemd/logind.nix
index b0c927f19f9..cf01c188285 100644
--- a/nixos/modules/system/boot/systemd/logind.nix
+++ b/nixos/modules/system/boot/systemd/logind.nix
@@ -11,64 +11,145 @@ let
   ];
 in
 {
-  options = {
-    services.logind.extraConfig = mkOption {
+  options.services.logind = {
+    extraConfig = mkOption {
       default = "";
       type = types.lines;
       example = "IdleAction=lock";
       description = lib.mdDoc ''
-        Extra config options for systemd-logind. See
-        [
-        logind.conf(5)](https://www.freedesktop.org/software/systemd/man/logind.conf.html) for available options.
+        Extra config options for systemd-logind.
+        See [logind.conf(5)](https://www.freedesktop.org/software/systemd/man/logind.conf.html)
+        for available options.
       '';
     };
 
-    services.logind.killUserProcesses = mkOption {
+    killUserProcesses = mkOption {
       default = false;
       type = types.bool;
       description = lib.mdDoc ''
         Specifies whether the processes of a user should be killed
         when the user logs out.  If true, the scope unit corresponding
         to the session and all processes inside that scope will be
-        terminated.  If false, the scope is "abandoned" (see
-        [systemd.scope(5)](https://www.freedesktop.org/software/systemd/man/systemd.scope.html#)), and processes are not killed.
+        terminated.  If false, the scope is "abandoned"
+        (see [systemd.scope(5)](https://www.freedesktop.org/software/systemd/man/systemd.scope.html#)),
+        and processes are not killed.
 
         See [logind.conf(5)](https://www.freedesktop.org/software/systemd/man/logind.conf.html#KillUserProcesses=)
         for more details.
       '';
     };
 
-    services.logind.lidSwitch = mkOption {
+    powerKey = mkOption {
+      default = "poweroff";
+      example = "ignore";
+      type = logindHandlerType;
+
+      description = lib.mdDoc ''
+        Specifies what to do when the power key is pressed.
+      '';
+    };
+
+    powerKeyLongPress = mkOption {
+      default = "ignore";
+      example = "reboot";
+      type = logindHandlerType;
+
+      description = lib.mdDoc ''
+        Specifies what to do when the power key is long-pressed.
+      '';
+    };
+
+    rebootKey = mkOption {
+      default = "reboot";
+      example = "ignore";
+      type = logindHandlerType;
+
+      description = lib.mdDoc ''
+        Specifies what to do when the reboot key is pressed.
+      '';
+    };
+
+    rebootKeyLongPress = mkOption {
+      default = "poweroff";
+      example = "ignore";
+      type = logindHandlerType;
+
+      description = lib.mdDoc ''
+        Specifies what to do when the reboot key is long-pressed.
+      '';
+    };
+
+    suspendKey = mkOption {
       default = "suspend";
       example = "ignore";
       type = logindHandlerType;
 
       description = lib.mdDoc ''
-        Specifies what to be done when the laptop lid is closed.
+        Specifies what to do when the suspend key is pressed.
+      '';
+    };
+
+    suspendKeyLongPress = mkOption {
+      default = "hibernate";
+      example = "ignore";
+      type = logindHandlerType;
+
+      description = lib.mdDoc ''
+        Specifies what to do when the suspend key is long-pressed.
+      '';
+    };
+
+    hibernateKey = mkOption {
+      default = "hibernate";
+      example = "ignore";
+      type = logindHandlerType;
+
+      description = lib.mdDoc ''
+        Specifies what to do when the hibernate key is pressed.
       '';
     };
 
-    services.logind.lidSwitchDocked = mkOption {
+    hibernateKeyLongPress = mkOption {
       default = "ignore";
       example = "suspend";
       type = logindHandlerType;
 
       description = lib.mdDoc ''
-        Specifies what to be done when the laptop lid is closed
-        and another screen is added.
+        Specifies what to do when the hibernate key is long-pressed.
       '';
     };
 
-    services.logind.lidSwitchExternalPower = mkOption {
+    lidSwitch = mkOption {
+      default = "suspend";
+      example = "ignore";
+      type = logindHandlerType;
+
+      description = lib.mdDoc ''
+        Specifies what to do when the laptop lid is closed.
+      '';
+    };
+
+    lidSwitchExternalPower = mkOption {
       default = cfg.lidSwitch;
       defaultText = literalExpression "services.logind.lidSwitch";
       example = "ignore";
       type = logindHandlerType;
 
       description = lib.mdDoc ''
-        Specifies what to do when the laptop lid is closed and the system is
-        on external power. By default use the same action as specified in
-        services.logind.lidSwitch.
+        Specifies what to do when the laptop lid is closed
+        and the system is on external power. By default use
+        the same action as specified in services.logind.lidSwitch.
+      '';
+    };
+
+    lidSwitchDocked = mkOption {
+      default = "ignore";
+      example = "suspend";
+      type = logindHandlerType;
+
+      description = lib.mdDoc ''
+        Specifies what to do when the laptop lid is closed
+        and another screen is added.
       '';
     };
   };
@@ -94,9 +175,17 @@ in
       "systemd/logind.conf".text = ''
         [Login]
         KillUserProcesses=${if cfg.killUserProcesses then "yes" else "no"}
+        HandlePowerKey=${cfg.powerKey}
+        HandlePowerKeyLongPress=${cfg.powerKeyLongPress}
+        HandleRebootKey=${cfg.rebootKey}
+        HandleRebootKeyLongPress=${cfg.rebootKeyLongPress}
+        HandleSuspendKey=${cfg.suspendKey}
+        HandleSuspendKeyLongPress=${cfg.suspendKeyLongPress}
+        HandleHibernateKey=${cfg.hibernateKey}
+        HandleHibernateKeyLongPress=${cfg.hibernateKeyLongPress}
         HandleLidSwitch=${cfg.lidSwitch}
-        HandleLidSwitchDocked=${cfg.lidSwitchDocked}
         HandleLidSwitchExternalPower=${cfg.lidSwitchExternalPower}
+        HandleLidSwitchDocked=${cfg.lidSwitchDocked}
         ${cfg.extraConfig}
       '';
     };
diff --git a/nixos/modules/system/boot/systemd/nspawn.nix b/nixos/modules/system/boot/systemd/nspawn.nix
index cbc89554c9f..b513aa051f2 100644
--- a/nixos/modules/system/boot/systemd/nspawn.nix
+++ b/nixos/modules/system/boot/systemd/nspawn.nix
@@ -11,7 +11,7 @@ let
     (assertOnlyFields [
       "Boot" "ProcessTwo" "Parameters" "Environment" "User" "WorkingDirectory"
       "PivotRoot" "Capability" "DropCapability" "NoNewPrivileges" "KillSignal"
-      "Personality" "MachineId" "PrivateUsers" "NotifyReady" "SystemCallFilter"
+      "Personality" "MachineID" "PrivateUsers" "NotifyReady" "SystemCallFilter"
       "LimitCPU" "LimitFSIZE" "LimitDATA" "LimitSTACK" "LimitCORE" "LimitRSS"
       "LimitNOFILE" "LimitAS" "LimitNPROC" "LimitMEMLOCK" "LimitLOCKS"
       "LimitSIGPENDING" "LimitMSGQUEUE" "LimitNICE" "LimitRTPRIO" "LimitRTTIME"
diff --git a/nixos/modules/system/boot/systemd/repart.nix b/nixos/modules/system/boot/systemd/repart.nix
new file mode 100644
index 00000000000..e81b3e4ff2a
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/repart.nix
@@ -0,0 +1,152 @@
+{ config, pkgs, lib, utils, ... }:
+
+let
+  cfg = config.systemd.repart;
+  initrdCfg = config.boot.initrd.systemd.repart;
+
+  writeDefinition = name: partitionConfig: pkgs.writeText
+    "${name}.conf"
+    (lib.generators.toINI { } { Partition = partitionConfig; });
+
+  listOfDefinitions = lib.mapAttrsToList
+    writeDefinition
+    (lib.filterAttrs (k: _: !(lib.hasPrefix "_" k)) cfg.partitions);
+
+  # Create a directory in the store that contains a copy of all definition
+  # files. This is then passed to systemd-repart in the initrd so it can access
+  # the definition files after the sysroot has been mounted but before
+  # activation. This needs a hard copy of the files and not just symlinks
+  # because otherwise the files do not show up in the sysroot.
+  definitionsDirectory = pkgs.runCommand "systemd-repart-definitions" { } ''
+    mkdir -p $out
+    ${(lib.concatStringsSep "\n"
+      (map (pkg: "cp ${pkg} $out/${pkg.name}") listOfDefinitions)
+    )}
+  '';
+in
+{
+  options = {
+    boot.initrd.systemd.repart = {
+      enable = lib.mkEnableOption (lib.mdDoc "systemd-repart") // {
+        description = lib.mdDoc ''
+          Grow and add partitions to a partition table at boot time in the initrd.
+          systemd-repart only works with GPT partition tables.
+
+          To run systemd-repart after the initrd, see
+          `options.systemd.repart.enable`.
+        '';
+      };
+
+      device = lib.mkOption {
+        type = with lib.types; nullOr str;
+        description = lib.mdDoc ''
+          The device to operate on.
+
+          If `device == null`, systemd-repart will operate on the device
+          backing the root partition. So in order to dynamically *create* the
+          root partition in the initrd you need to set a device.
+        '';
+        default = null;
+        example = "/dev/vda";
+      };
+    };
+
+    systemd.repart = {
+      enable = lib.mkEnableOption (lib.mdDoc "systemd-repart") // {
+        description = lib.mdDoc ''
+          Grow and add partitions to a partition table.
+          systemd-repart only works with GPT partition tables.
+
+          To run systemd-repart while in the initrd, see
+          `options.boot.initrd.systemd.repart.enable`.
+        '';
+      };
+
+      partitions = lib.mkOption {
+        type = with lib.types; attrsOf (attrsOf (oneOf [ str int bool ]));
+        default = { };
+        example = {
+          "10-root" = {
+            Type = "root";
+          };
+          "20-home" = {
+            Type = "home";
+            SizeMinBytes = "512M";
+            SizeMaxBytes = "2G";
+          };
+        };
+        description = lib.mdDoc ''
+          Specify partitions as a set of the names of the definition files as the
+          key and the partition configuration as its value. The partition
+          configuration can use all upstream options. See <link
+          xlink:href="https://www.freedesktop.org/software/systemd/man/repart.d.html"/>
+          for all available options.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf (cfg.enable || initrdCfg.enable) {
+    boot.initrd.systemd = lib.mkIf initrdCfg.enable {
+      additionalUpstreamUnits = [
+        "systemd-repart.service"
+      ];
+
+      storePaths = [
+        "${config.boot.initrd.systemd.package}/bin/systemd-repart"
+      ];
+
+      contents."/etc/repart.d".source = definitionsDirectory;
+
+      # Override defaults in upstream unit.
+      services.systemd-repart =
+        let
+          deviceUnit = "${utils.escapeSystemdPath initrdCfg.device}.device";
+        in
+        {
+          # systemd-repart tries to create directories in /var/tmp by default to
+          # store large temporary files that benefit from persistence on disk. In
+          # the initrd, however, /var/tmp does not provide more persistence than
+          # /tmp, so we re-use it here.
+          environment."TMPDIR" = "/tmp";
+          serviceConfig = {
+            ExecStart = [
+              " " # required to unset the previous value.
+              # When running in the initrd, systemd-repart by default searches
+              # for definition files in /sysroot or /sysusr. We tell it to look
+              # in the initrd itself.
+              ''${config.boot.initrd.systemd.package}/bin/systemd-repart \
+                  --definitions=/etc/repart.d \
+                  --dry-run=no ${lib.optionalString (initrdCfg.device != null) initrdCfg.device}
+              ''
+            ];
+          };
+          # systemd-repart needs to run after /sysroot (or /sysuser, but we
+          # don't have it) has been mounted because otherwise it cannot
+          # determine the device (i.e disk) to operate on. If you want to run
+          # systemd-repart without /sysroot (i.e. to create the root
+          # partition), you have to explicitly tell it which device to operate
+          # on. The service then needs to be ordered to run after this device
+          # is available.
+          requires = lib.mkIf (initrdCfg.device != null) [ deviceUnit ];
+          after =
+            if initrdCfg.device == null then
+              [ "sysroot.mount" ]
+            else
+              [ deviceUnit ];
+        };
+    };
+
+    environment.etc = lib.mkIf cfg.enable {
+      "repart.d".source = definitionsDirectory;
+    };
+
+    systemd = lib.mkIf cfg.enable {
+      additionalUpstreamSystemUnits = [
+        "systemd-repart.service"
+      ];
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ nikstur ];
+}
diff --git a/nixos/modules/system/boot/systemd/user.nix b/nixos/modules/system/boot/systemd/user.nix
index 46d66fe4e68..92e1b087392 100644
--- a/nixos/modules/system/boot/systemd/user.nix
+++ b/nixos/modules/system/boot/systemd/user.nix
@@ -39,6 +39,20 @@ let
     "timers.target"
     "xdg-desktop-autostart.target"
   ] ++ config.systemd.additionalUpstreamUserUnits;
+
+  writeTmpfiles = { rules, user ? null }:
+    let
+      suffix = if user == null then "" else "-${user}";
+    in
+    pkgs.writeTextFile {
+      name = "nixos-user-tmpfiles.d${suffix}";
+      destination = "/etc/xdg/user-tmpfiles.d/00-nixos${suffix}.conf";
+      text = ''
+        # This file is created automatically and should not be modified.
+        # Please change the options ‘systemd.user.tmpfiles’ instead.
+        ${concatStringsSep "\n" rules}
+      '';
+    };
 in {
   options = {
     systemd.user.extraConfig = mkOption {
@@ -46,7 +60,7 @@ in {
       type = types.lines;
       example = "DefaultCPUAccounting=yes";
       description = lib.mdDoc ''
-        Extra config options for systemd user instances. See man systemd-user.conf for
+        Extra config options for systemd user instances. See {manpage}`systemd-user.conf(5)` for
         available options.
       '';
     };
@@ -93,6 +107,43 @@ in {
       description = lib.mdDoc "Definition of systemd per-user timer units.";
     };
 
+    systemd.user.tmpfiles = {
+      rules = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "D %C - - - 7d" ];
+        description = lib.mdDoc ''
+          Global user rules for creation, deletion and cleaning of volatile and
+          temporary files automatically. See
+          {manpage}`tmpfiles.d(5)`
+          for the exact format.
+        '';
+      };
+
+      users = mkOption {
+        description = mdDoc ''
+          Per-user rules for creation, deletion and cleaning of volatile and
+          temporary files automatically.
+        '';
+        default = {};
+        type = types.attrsOf (types.submodule {
+          options = {
+            rules = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "D %C - - - 7d" ];
+              description = mdDoc ''
+                Per-user rules for creation, deletion and cleaning of volatile and
+                temporary files automatically. See
+                {manpage}`tmpfiles.d(5)`
+                for the exact format.
+              '';
+            };
+          };
+        });
+      };
+    };
+
     systemd.additionalUpstreamUserUnits = mkOption {
       default = [];
       type = types.listOf types.str;
@@ -154,5 +205,30 @@ in {
     # Some overrides to upstream units.
     systemd.services."user@".restartIfChanged = false;
     systemd.services.systemd-user-sessions.restartIfChanged = false; # Restart kills all active sessions.
+
+    # enable systemd user tmpfiles
+    systemd.user.services.systemd-tmpfiles-setup.wantedBy =
+      optional
+        (cfg.tmpfiles.rules != [] || any (cfg': cfg'.rules != []) (attrValues cfg.tmpfiles.users))
+        "basic.target";
+
+    # /run/current-system/sw/etc/xdg is in systemd's $XDG_CONFIG_DIRS so we can
+    # write the tmpfiles.d rules for everyone there
+    environment.systemPackages =
+      optional
+        (cfg.tmpfiles.rules != [])
+        (writeTmpfiles { inherit (cfg.tmpfiles) rules; });
+
+    # /etc/profiles/per-user/$USER/etc/xdg is in systemd's $XDG_CONFIG_DIRS so
+    # we can write a single user's tmpfiles.d rules there
+    users.users =
+      mapAttrs
+        (user: cfg': {
+          packages = optional (cfg'.rules != []) (writeTmpfiles {
+            inherit (cfg') rules;
+            inherit user;
+          });
+        })
+        cfg.tmpfiles.users;
   };
 }
diff --git a/nixos/modules/system/boot/systemd/userdbd.nix b/nixos/modules/system/boot/systemd/userdbd.nix
new file mode 100644
index 00000000000..994aa3ca3b8
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/userdbd.nix
@@ -0,0 +1,18 @@
+{ config, lib, ... }:
+
+let
+  cfg = config.services.userdbd;
+in
+{
+  options.services.userdbd.enable = lib.mkEnableOption (lib.mdDoc ''
+    Enables the systemd JSON user/group record lookup service
+  '');
+  config = lib.mkIf cfg.enable {
+    systemd.additionalUpstreamSystemUnits = [
+      "systemd-userdbd.socket"
+      "systemd-userdbd.service"
+    ];
+
+    systemd.sockets.systemd-userdbd.wantedBy = [ "sockets.target" ];
+  };
+}
diff --git a/nixos/modules/system/boot/tmp.nix b/nixos/modules/system/boot/tmp.nix
index 1f9431710ae..fd16cd3fba4 100644
--- a/nixos/modules/system/boot/tmp.nix
+++ b/nixos/modules/system/boot/tmp.nix
@@ -3,62 +3,67 @@
 with lib;
 
 let
-  cfg = config.boot;
+  cfg = config.boot.tmp;
 in
 {
-
-  ###### interface
+  imports = [
+    (mkRenamedOptionModule [ "boot" "cleanTmpDir" ] [ "boot" "tmp" "cleanOnBoot" ])
+    (mkRenamedOptionModule [ "boot" "tmpOnTmpfs" ] [ "boot" "tmp" "useTmpfs" ])
+    (mkRenamedOptionModule [ "boot" "tmpOnTmpfsSize" ] [ "boot" "tmp" "tmpfsSize" ])
+  ];
 
   options = {
+    boot.tmp = {
+      cleanOnBoot = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Whether to delete all files in {file}`/tmp` during boot.
+        '';
+      };
 
-    boot.cleanTmpDir = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Whether to delete all files in {file}`/tmp` during boot.
-      '';
-    };
+      tmpfsSize = mkOption {
+        type = types.oneOf [ types.str types.types.ints.positive ];
+        default = "50%";
+        description = lib.mdDoc ''
+          Size of tmpfs in percentage.
+          Percentage is defined by systemd.
+        '';
+      };
 
-    boot.tmpOnTmpfs = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-         Whether to mount a tmpfs on {file}`/tmp` during boot.
-      '';
-    };
+      useTmpfs = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+           Whether to mount a tmpfs on {file}`/tmp` during boot.
 
-    boot.tmpOnTmpfsSize = mkOption {
-      type = types.oneOf [ types.str types.types.ints.positive ];
-      default = "50%";
-      description = lib.mdDoc ''
-        Size of tmpfs in percentage.
-        Percentage is defined by systemd.
-      '';
+           ::: {.note}
+           Large Nix builds can fail if the mounted tmpfs is not large enough.
+           In such a case either increase the tmpfsSize or disable this option.
+           :::
+        '';
+      };
     };
-
   };
 
-  ###### implementation
-
   config = {
-
     # When changing remember to update /tmp mount in virtualisation/qemu-vm.nix
-    systemd.mounts = mkIf cfg.tmpOnTmpfs [
+    systemd.mounts = mkIf cfg.useTmpfs [
       {
         what = "tmpfs";
         where = "/tmp";
         type = "tmpfs";
-        mountConfig.Options = concatStringsSep "," [ "mode=1777"
-                                                     "strictatime"
-                                                     "rw"
-                                                     "nosuid"
-                                                     "nodev"
-                                                     "size=${toString cfg.tmpOnTmpfsSize}" ];
+        mountConfig.Options = concatStringsSep "," [
+          "mode=1777"
+          "strictatime"
+          "rw"
+          "nosuid"
+          "nodev"
+          "size=${toString cfg.tmpfsSize}"
+        ];
       }
     ];
 
-    systemd.tmpfiles.rules = optional config.boot.cleanTmpDir "D! /tmp 1777 root root";
-
+    systemd.tmpfiles.rules = optional cfg.cleanOnBoot "D! /tmp 1777 root root";
   };
-
 }
diff --git a/nixos/modules/system/etc/setup-etc.pl b/nixos/modules/system/etc/setup-etc.pl
index a048261a3df..ea0a3830817 100644
--- a/nixos/modules/system/etc/setup-etc.pl
+++ b/nixos/modules/system/etc/setup-etc.pl
@@ -13,8 +13,12 @@ sub atomicSymlink {
     my $tmp = "$target.tmp";
     unlink $tmp;
     symlink $source, $tmp or return 0;
-    rename $tmp, $target or return 0;
-    return 1;
+    if (rename $tmp, $target) {
+        return 1;
+    } else {
+        unlink $tmp;
+        return 0;
+    }
 }
 
 
@@ -87,6 +91,12 @@ my @copied;
 
 sub link {
     my $fn = substr $File::Find::name, length($etc) + 1 or next;
+
+    # nixos-enter sets up /etc/resolv.conf as a bind mount, so skip it.
+    if ($fn eq "resolv.conf" and $ENV{'IN_NIXOS_ENTER'}) {
+        return;
+    }
+
     my $target = "/etc/$fn";
     File::Path::make_path(dirname $target);
     $created{$fn} = 1;
@@ -103,7 +113,7 @@ sub link {
     if (-e "$_.mode") {
         my $mode = read_file("$_.mode"); chomp $mode;
         if ($mode eq "direct-symlink") {
-            atomicSymlink readlink("$static/$fn"), $target or warn;
+            atomicSymlink readlink("$static/$fn"), $target or warn "could not create symlink $target";
         } else {
             my $uid = read_file("$_.uid"); chomp $uid;
             my $gid = read_file("$_.gid"); chomp $gid;
@@ -112,12 +122,15 @@ sub link {
             $gid = getgrnam $gid unless $gid =~ /^\+/;
             chown int($uid), int($gid), "$target.tmp" or warn;
             chmod oct($mode), "$target.tmp" or warn;
-            rename "$target.tmp", $target or warn;
+            unless (rename "$target.tmp", $target) {
+                warn "could not create target $target";
+                unlink "$target.tmp";
+            }
         }
         push @copied, $fn;
         print CLEAN "$fn\n";
     } elsif (-l "$_") {
-        atomicSymlink "$static/$fn", $target or warn;
+        atomicSymlink "$static/$fn", $target or warn "could not create symlink $target";
     }
 }
 
diff --git a/nixos/modules/tasks/bcache.nix b/nixos/modules/tasks/bcache.nix
index 408ddc02373..35b922dc8a1 100644
--- a/nixos/modules/tasks/bcache.nix
+++ b/nixos/modules/tasks/bcache.nix
@@ -1,8 +1,12 @@
 { config, lib, pkgs, ... }:
 
 {
-  options.boot.initrd.services.bcache.enable = (lib.mkEnableOption (lib.mdDoc "bcache support in the initrd")) // {
-    visible = false; # only works with systemd stage 1
+  options.boot.initrd.services.bcache.enable = lib.mkEnableOption (lib.mdDoc "bcache support in the initrd") // {
+    description = lib.mdDoc ''
+      *This will only be used when systemd is used in stage 1.*
+
+      Whether to enable bcache support in the initrd.
+    '';
   };
 
   config = {
diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix
index 7f2c8a41b20..7cb2ca23fa4 100644
--- a/nixos/modules/tasks/filesystems.nix
+++ b/nixos/modules/tasks/filesystems.nix
@@ -36,6 +36,15 @@ let
         description = lib.mdDoc "Location of the mounted file system.";
       };
 
+      stratis.poolUuid = lib.mkOption {
+        type = types.uniq (types.nullOr types.str);
+        description = lib.mdDoc ''
+          UUID of the stratis pool that the fs is located in
+        '';
+        example = "04c68063-90a5-4235-b9dd-6180098a20d9";
+        default = null;
+      };
+
       device = mkOption {
         default = null;
         example = "/dev/sda";
@@ -103,12 +112,9 @@ let
       };
 
       formatOptions = mkOption {
-        default = "";
-        type = types.str;
-        description = lib.mdDoc ''
-          If {option}`autoFormat` option is set specifies
-          extra options passed to mkfs.
-        '';
+        visible = false;
+        type = types.unspecified;
+        default = null;
       };
 
       autoResize = mkOption {
@@ -130,19 +136,11 @@ let
 
     };
 
-    config = let
-      defaultFormatOptions =
-        # -F needed to allow bare block device without partitions
-        if (builtins.substring 0 3 config.fsType) == "ext" then "-F"
-        # -q needed for non-interactive operations
-        else if config.fsType == "jfs" then "-q"
-        # (same here)
-        else if config.fsType == "reiserfs" then "-q"
-        else null;
-    in {
-      options = mkIf config.autoResize [ "x-nixos.autoresize" ];
-      formatOptions = mkIf (defaultFormatOptions != null) (mkDefault defaultFormatOptions);
-    };
+    config.options = mkMerge [
+      (mkIf config.autoResize [ "x-systemd.growfs" ])
+      (mkIf config.autoFormat [ "x-systemd.makefs" ])
+      (mkIf (utils.fsNeededForBoot config) [ "x-initrd.mount" ])
+    ];
 
   };
 
@@ -155,30 +153,54 @@ let
 
   makeFstabEntries =
     let
-      fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "nfs4" "vboxsf" "glusterfs" "apfs" "9p" "cifs" "prl_fs" "vmhgfs" ];
+      fsToSkipCheck = [
+        "none"
+        "auto"
+        "overlay"
+        "iso9660"
+        "bindfs"
+        "udf"
+        "btrfs"
+        "zfs"
+        "tmpfs"
+        "bcachefs"
+        "nfs"
+        "nfs4"
+        "nilfs2"
+        "vboxsf"
+        "squashfs"
+        "glusterfs"
+        "apfs"
+        "9p"
+        "cifs"
+        "prl_fs"
+        "vmhgfs"
+      ] ++ lib.optionals (!config.boot.initrd.checkJournalingFS) [
+        "ext3"
+        "ext4"
+        "reiserfs"
+        "xfs"
+        "jfs"
+        "f2fs"
+      ];
       isBindMount = fs: builtins.elem "bind" fs.options;
       skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck || isBindMount fs;
       # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces
       escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string;
-    in fstabFileSystems: { rootPrefix ? "", excludeChecks ? false, extraOpts ? (fs: []) }: concatMapStrings (fs:
+    in fstabFileSystems: { rootPrefix ? "" }: concatMapStrings (fs:
       (optionalString (isBindMount fs) (escape rootPrefix))
       + (if fs.device != null then escape fs.device
          else if fs.label != null then "/dev/disk/by-label/${escape fs.label}"
          else throw "No device specified for mount point ‘${fs.mountPoint}’.")
-      + " " + escape (rootPrefix + fs.mountPoint)
+      + " " + escape fs.mountPoint
       + " " + fs.fsType
-      + " " + escape (builtins.concatStringsSep "," (fs.options ++ (extraOpts fs)))
-      + " " + (optionalString (!excludeChecks)
-        ("0 " + (if skipCheck fs then "0" else if fs.mountPoint == "/" then "1" else "2")))
+      + " " + escape (builtins.concatStringsSep "," fs.options)
+      + " 0 " + (if skipCheck fs then "0" else if fs.mountPoint == "/" then "1" else "2")
       + "\n"
     ) fstabFileSystems;
 
     initrdFstab = pkgs.writeText "initrd-fstab" (makeFstabEntries (filter utils.fsNeededForBoot fileSystems) {
       rootPrefix = "/sysroot";
-      excludeChecks = true;
-      extraOpts = fs:
-        (optional fs.autoResize "x-systemd.growfs")
-        ++ (optional fs.autoFormat "x-systemd.makefs");
     });
 
 in
@@ -280,7 +302,13 @@ in
 
     assertions = let
       ls = sep: concatMapStringsSep sep (x: x.mountPoint);
-      notAutoResizable = fs: fs.autoResize && !(hasPrefix "ext" fs.fsType || fs.fsType == "f2fs");
+      resizableFSes = [
+        "ext3"
+        "ext4"
+        "btrfs"
+        "xfs"
+      ];
+      notAutoResizable = fs: fs.autoResize && !(builtins.elem fs.fsType resizableFSes);
     in [
       { assertion = ! (fileSystems' ? cycle);
         message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}";
@@ -288,8 +316,21 @@ in
       { assertion = ! (any notAutoResizable fileSystems);
         message = let
           fs = head (filter notAutoResizable fileSystems);
-        in
-          "Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = \"${fs.fsType}\"':${if fs.fsType == "auto" then " fsType has to be explicitly set and" else ""} only the ext filesystems and f2fs support it.";
+        in ''
+          Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = "${fs.fsType}"'
+          ${optionalString (fs.fsType == "auto") "fsType has to be explicitly set and"}
+          only the following support it: ${lib.concatStringsSep ", " resizableFSes}.
+        '';
+      }
+      {
+        assertion = ! (any (fs: fs.formatOptions != null) fileSystems);
+        message = let
+          fs = head (filter (fs: fs.formatOptions != null) fileSystems);
+        in ''
+          'fileSystems.<name>.formatOptions' has been removed, since
+          systemd-makefs does not support any way to provide formatting
+          options.
+        '';
       }
     ];
 
@@ -328,7 +369,9 @@ in
         )}
       '';
 
-    boot.initrd.systemd.contents."/etc/fstab".source = initrdFstab;
+    boot.initrd.systemd.storePaths = [initrdFstab];
+    boot.initrd.systemd.managerEnvironment.SYSTEMD_SYSROOT_FSTAB = initrdFstab;
+    boot.initrd.systemd.services.initrd-parse-etc.environment.SYSTEMD_SYSROOT_FSTAB = initrdFstab;
 
     # Provide a target that pulls in all filesystems.
     systemd.targets.fs =
@@ -336,37 +379,7 @@ in
         wants = [ "local-fs.target" "remote-fs.target" ];
       };
 
-    systemd.services =
-
-    # Emit systemd services to format requested filesystems.
-      let
-        formatDevice = fs:
-          let
-            mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount";
-            device'  = escapeSystemdPath fs.device;
-            device'' = "${device'}.device";
-          in nameValuePair "mkfs-${device'}"
-          { description = "Initialisation of Filesystem ${fs.device}";
-            wantedBy = [ mountPoint' ];
-            before = [ mountPoint' "systemd-fsck@${device'}.service" ];
-            requires = [ device'' ];
-            after = [ device'' ];
-            path = [ pkgs.util-linux ] ++ config.system.fsPackages;
-            script =
-              ''
-                if ! [ -e "${fs.device}" ]; then exit 1; fi
-                # FIXME: this is scary.  The test could be more robust.
-                type=$(blkid -p -s TYPE -o value "${fs.device}" || true)
-                if [ -z "$type" ]; then
-                  echo "creating ${fs.fsType} filesystem on ${fs.device}..."
-                  mkfs.${fs.fsType} ${fs.formatOptions} "${fs.device}"
-                fi
-              '';
-            unitConfig.RequiresMountsFor = [ "${dirOf fs.device}" ];
-            unitConfig.DefaultDependencies = false; # needed to prevent a cycle
-            serviceConfig.Type = "oneshot";
-          };
-      in listToAttrs (map formatDevice (filter (fs: fs.autoFormat && !(utils.fsNeededForBoot fs)) fileSystems)) // {
+    systemd.services = {
     # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore.
     # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then.
         "mount-pstore" = {
diff --git a/nixos/modules/tasks/filesystems/bcachefs.nix b/nixos/modules/tasks/filesystems/bcachefs.nix
index e3ad52a7b05..19ef188ce78 100644
--- a/nixos/modules/tasks/filesystems/bcachefs.nix
+++ b/nixos/modules/tasks/filesystems/bcachefs.nix
@@ -6,6 +6,15 @@ let
 
   bootFs = filterAttrs (n: fs: (fs.fsType == "bcachefs") && (utils.fsNeededForBoot fs)) config.fileSystems;
 
+  mountCommand = pkgs.runCommand "mount.bcachefs" {} ''
+    mkdir -p $out/bin
+    cat > $out/bin/mount.bcachefs <<EOF
+    #!/bin/sh
+    exec "/bin/bcachefs" mount "\$@"
+    EOF
+    chmod +x $out/bin/mount.bcachefs
+  '';
+
   commonFunctions = ''
     prompt() {
         local name="$1"
@@ -16,7 +25,7 @@ let
         local path="$2"
         if bcachefs unlock -c $path > /dev/null 2> /dev/null; then    # test for encryption
             prompt $name
-            until bcachefs unlock $path 2> /dev/null; do              # repeat until sucessfully unlocked
+            until bcachefs unlock $path 2> /dev/null; do              # repeat until successfully unlocked
                 printf "unlocking failed!\n"
                 prompt $name
             done
@@ -58,13 +67,12 @@ in
 
       boot.initrd.systemd.extraBin = {
         "bcachefs" = "${pkgs.bcachefs-tools}/bin/bcachefs";
-        "mount.bcachefs" = pkgs.runCommand "mount.bcachefs" {} ''
-          cp -pdv ${pkgs.bcachefs-tools}/bin/.mount.bcachefs.sh-wrapped $out
-        '';
+        "mount.bcachefs" = "${mountCommand}/bin/mount.bcachefs";
       };
 
       boot.initrd.extraUtilsCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ''
         copy_bin_and_libs ${pkgs.bcachefs-tools}/bin/bcachefs
+        copy_bin_and_libs ${mountCommand}/bin/mount.bcachefs
       '';
       boot.initrd.extraUtilsCommandsTest = ''
         $out/bin/bcachefs version
diff --git a/nixos/modules/tasks/filesystems/btrfs.nix b/nixos/modules/tasks/filesystems/btrfs.nix
index bd85a1f8d1f..82fdd605871 100644
--- a/nixos/modules/tasks/filesystems/btrfs.nix
+++ b/nixos/modules/tasks/filesystems/btrfs.nix
@@ -25,7 +25,7 @@ in
         type = types.listOf types.path;
         example = [ "/" ];
         description = lib.mdDoc ''
-          List of paths to btrfs filesystems to regularily call {command}`btrfs scrub` on.
+          List of paths to btrfs filesystems to regularly call {command}`btrfs scrub` on.
           Defaults to all mount points with btrfs filesystems.
           If you mount a filesystem multiple times or additionally mount subvolumes,
           you need to manually specify this list to avoid scrubbing multiple times.
diff --git a/nixos/modules/tasks/filesystems/envfs.nix b/nixos/modules/tasks/filesystems/envfs.nix
index ef8f655c532..365cb46ff2f 100644
--- a/nixos/modules/tasks/filesystems/envfs.nix
+++ b/nixos/modules/tasks/filesystems/envfs.nix
@@ -7,17 +7,18 @@ let
       device = "none";
       fsType = "envfs";
       options = [
-        "fallback-path=${pkgs.runCommand "fallback-path" {} ''
+        "fallback-path=${pkgs.runCommand "fallback-path" {} (''
           mkdir -p $out
-          ln -s ${pkgs.coreutils}/bin/env $out/env
-          ln -s ${config.system.build.binsh}/bin/sh $out/sh
-        ''}"
+          ln -s ${config.environment.usrbinenv} $out/env
+          ln -s ${config.environment.binsh} $out/sh
+        '' + cfg.extraFallbackPathCommands)}"
+        "nofail"
       ];
     };
     "/bin" = {
       device = "/usr/bin";
       fsType = "none";
-      options = [ "bind" ];
+      options = [ "bind" "nofail" ];
     };
   };
 in {
@@ -31,11 +32,19 @@ in {
           etc.
         '';
       };
+
       package = lib.mkOption {
         type = lib.types.package;
-        description = lib.mdDoc "Which package to use for the envfs.";
         default = pkgs.envfs;
-        defaultText = lib.mdDoc "pkgs.envfs";
+        defaultText = lib.literalExpression "pkgs.envfs";
+        description = lib.mdDoc "Which package to use for the envfs.";
+      };
+
+      extraFallbackPathCommands = lib.mkOption {
+        type = lib.types.lines;
+        default = "";
+        example = "ln -s $''{pkgs.bash}/bin/bash $out/bash";
+        description = lib.mdDoc "Extra commands to run in the package that contains fallback executables in case not other executable is found";
       };
     };
   };
diff --git a/nixos/modules/tasks/filesystems/erofs.nix b/nixos/modules/tasks/filesystems/erofs.nix
new file mode 100644
index 00000000000..a3d65766935
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/erofs.nix
@@ -0,0 +1,21 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  inInitrd = lib.any (fs: fs == "erofs") config.boot.initrd.supportedFilesystems;
+  inSystem = lib.any (fs: fs == "erofs") config.boot.supportedFilesystems;
+
+in
+
+{
+  config = lib.mkIf (inInitrd || inSystem) {
+
+    system.fsPackages = [ pkgs.erofs-utils ];
+
+    boot.initrd.availableKernelModules = lib.mkIf inInitrd [ "erofs" ];
+
+    # fsck.erofs is currently experimental and should not be run as a
+    # privileged user. Thus, it is not included in the initrd.
+
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/f2fs.nix b/nixos/modules/tasks/filesystems/f2fs.nix
index 1d52861aa39..035784f43df 100644
--- a/nixos/modules/tasks/filesystems/f2fs.nix
+++ b/nixos/modules/tasks/filesystems/f2fs.nix
@@ -15,11 +15,6 @@ in
 
     boot.initrd.extraUtilsCommands = mkIf (inInitrd && !config.boot.initrd.systemd.enable) ''
       copy_bin_and_libs ${pkgs.f2fs-tools}/sbin/fsck.f2fs
-      ${optionalString (any (fs: fs.autoResize) fileSystems) ''
-        # We need f2fs-tools' tools to resize filesystems
-        copy_bin_and_libs ${pkgs.f2fs-tools}/sbin/resize.f2fs
-      ''}
-
     '';
   };
 }
diff --git a/nixos/modules/tasks/filesystems/squashfs.nix b/nixos/modules/tasks/filesystems/squashfs.nix
new file mode 100644
index 00000000000..10d45a21d3c
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/squashfs.nix
@@ -0,0 +1,13 @@
+{ config, lib, ... }:
+
+let
+
+  inInitrd = lib.any (fs: fs == "squashfs") config.boot.initrd.supportedFilesystems;
+
+in
+
+{
+
+  boot.initrd.availableKernelModules = lib.mkIf inInitrd [ "squashfs" ];
+
+}
diff --git a/nixos/modules/tasks/filesystems/vfat.nix b/nixos/modules/tasks/filesystems/vfat.nix
index 5baab1c802c..5421b617b43 100644
--- a/nixos/modules/tasks/filesystems/vfat.nix
+++ b/nixos/modules/tasks/filesystems/vfat.nix
@@ -11,7 +11,7 @@ in
 {
   config = mkIf (any (fs: fs == "vfat") config.boot.supportedFilesystems) {
 
-    system.fsPackages = [ pkgs.dosfstools ];
+    system.fsPackages = [ pkgs.dosfstools pkgs.mtools ];
 
     boot.initrd.kernelModules = mkIf inInitrd [ "vfat" "nls_cp437" "nls_iso8859-1" ];
 
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
index 6c775964751..72b937f3732 100644
--- a/nixos/modules/tasks/filesystems/zfs.nix
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -137,14 +137,15 @@ let
         awkCmd = "${pkgs.gawk}/bin/awk";
         inherit cfgZfs;
       }) + ''
-        poolImported "${pool}" && exit
-        echo -n "importing ZFS pool \"${pool}\"..."
-        # Loop across the import until it succeeds, because the devices needed may not be discovered yet.
-        for trial in `seq 1 60`; do
-          poolReady "${pool}" && poolImport "${pool}" && break
-          sleep 1
-        done
-        poolImported "${pool}" || poolImport "${pool}"  # Try one last time, e.g. to import a degraded pool.
+        if ! poolImported "${pool}"; then
+          echo -n "importing ZFS pool \"${pool}\"..."
+          # Loop across the import until it succeeds, because the devices needed may not be discovered yet.
+          for trial in `seq 1 60`; do
+            poolReady "${pool}" && poolImport "${pool}" && break
+            sleep 1
+          done
+          poolImported "${pool}" || poolImport "${pool}"  # Try one last time, e.g. to import a degraded pool.
+        fi
         if poolImported "${pool}"; then
           ${optionalString keyLocations.hasKeys ''
             ${keyLocations.command} | while IFS=$'\t' read ds kl ks; do
@@ -159,7 +160,7 @@ let
                   tries=3
                   success=false
                   while [[ $success != true ]] && [[ $tries -gt 0 ]]; do
-                    ${systemd}/bin/systemd-ask-password "Enter key for $ds:" | ${cfgZfs.package}/sbin/zfs load-key "$ds" \
+                    ${systemd}/bin/systemd-ask-password --timeout=${toString cfgZfs.passwordTimeout} "Enter key for $ds:" | ${cfgZfs.package}/sbin/zfs load-key "$ds" \
                       && success=true \
                       || tries=$((tries - 1))
                   done
@@ -312,6 +313,40 @@ in
           an interactive prompt (keylocation=prompt) and from a file (keylocation=file://).
         '';
       };
+
+      passwordTimeout = mkOption {
+        type = types.int;
+        default = 0;
+        description = lib.mdDoc ''
+          Timeout in seconds to wait for password entry for decrypt at boot.
+
+          Defaults to 0, which waits forever.
+        '';
+      };
+
+      removeLinuxDRM = lib.mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Linux 6.2 dropped some kernel symbols required on aarch64 required by zfs.
+          Enabling this option will bring them back to allow this kernel version.
+          Note that in some jurisdictions this may be illegal as it might be considered
+          removing copyright protection from the code.
+          See https://www.ifross.org/?q=en/artikel/ongoing-dispute-over-value-exportsymbolgpl-function for further information.
+
+          If configure your kernel package with `zfs.latestCompatibleLinuxPackages`, you will need to also pass removeLinuxDRM to that package like this:
+
+          ```
+          { pkgs, ... }: {
+            boot.kernelPackages = (pkgs.zfs.override {
+              removeLinuxDRM = pkgs.hostPlatform.isAarch64;
+            }).latestCompatibleLinuxPackages;
+
+            boot.zfs.removeLinuxDRM = true;
+          }
+          ```
+        '';
+      };
     };
 
     services.zfs.autoSnapshot = {
@@ -512,6 +547,15 @@ in
           assertion = cfgZfs.allowHibernation -> !cfgZfs.forceImportRoot && !cfgZfs.forceImportAll;
           message = "boot.zfs.allowHibernation while force importing is enabled will cause data corruption";
         }
+        {
+          assertion = !(elem "" allPools);
+          message = ''
+            Automatic pool detection found an empty pool name, which can't be used.
+            Hint: for `fileSystems` entries with `fsType = zfs`, the `device` attribute
+            should be a zfs dataset name, like `device = "pool/data/set"`.
+            This error can be triggered by using an absolute path, such as `"/dev/disk/..."`.
+          '';
+        }
       ];
 
       boot = {
@@ -521,11 +565,13 @@ in
         # https://github.com/NixOS/nixpkgs/issues/106093
         kernelParams = lib.optionals (!config.boot.zfs.allowHibernation) [ "nohibernate" ];
 
-        extraModulePackages = [
-          (if config.boot.zfs.enableUnstable then
+        extraModulePackages = let
+          kernelPkg = if config.boot.zfs.enableUnstable then
             config.boot.kernelPackages.zfsUnstable
            else
-            config.boot.kernelPackages.zfs)
+            config.boot.kernelPackages.zfs;
+        in [
+          (kernelPkg.override { inherit (cfgZfs) removeLinuxDRM; })
         ];
       };
 
@@ -643,6 +689,21 @@ in
       services.udev.packages = [ cfgZfs.package ]; # to hook zvol naming, etc.
       systemd.packages = [ cfgZfs.package ];
 
+      # Export kernel_neon_* symbols again.
+      # This change is necessary until ZFS figures out a solution
+      # with upstream or in their build system to fill the gap for
+      # this symbol.
+      # In the meantime, we restore what was once a working piece of code
+      # in the kernel.
+      boot.kernelPatches = lib.optional (cfgZfs.removeLinuxDRM && pkgs.stdenv.hostPlatform.system == "aarch64-linux") {
+        name = "export-neon-symbols-as-gpl";
+        patch = pkgs.fetchpatch {
+          url = "https://github.com/torvalds/linux/commit/aaeca98456431a8d9382ecf48ac4843e252c07b3.patch";
+          hash = "sha256-L2g4G1tlWPIi/QRckMuHDcdWBcKpObSWSRTvbHRIwIk=";
+          revert = true;
+        };
+      };
+
       systemd.services = let
         createImportService' = pool: createImportService {
           inherit pool;
diff --git a/nixos/modules/tasks/lvm.nix b/nixos/modules/tasks/lvm.nix
index a14f26c02e4..325a5aa45b1 100644
--- a/nixos/modules/tasks/lvm.nix
+++ b/nixos/modules/tasks/lvm.nix
@@ -25,8 +25,12 @@ in {
     boot.vdo.enable = mkEnableOption (lib.mdDoc "support for booting from VDOLVs");
   };
 
-  options.boot.initrd.services.lvm.enable = (mkEnableOption (lib.mdDoc "enable booting from LVM2 in the initrd")) // {
-    visible = false;
+  options.boot.initrd.services.lvm.enable = mkEnableOption (lib.mdDoc "booting from LVM2 in the initrd") // {
+    description = lib.mdDoc ''
+      *This will only be used when systemd is used in stage 1.*
+
+      Whether to enable booting from LVM2 in the initrd.
+    '';
   };
 
   config = mkMerge [
@@ -40,12 +44,13 @@ in {
       systemd.packages = [ cfg.package ];
 
       services.udev.packages = [ cfg.package.out ];
-
+    })
+    (mkIf config.boot.initrd.services.lvm.enable {
       # We need lvm2 for the device-mapper rules
-      boot.initrd.services.udev.packages = lib.mkIf config.boot.initrd.services.lvm.enable [ cfg.package ];
+      boot.initrd.services.udev.packages = [ cfg.package ];
       # The device-mapper rules want to call tools from lvm2
-      boot.initrd.systemd.initrdBin = lib.mkIf config.boot.initrd.services.lvm.enable [ cfg.package ];
-      boot.initrd.services.udev.binPackages = lib.mkIf config.boot.initrd.services.lvm.enable [ cfg.package ];
+      boot.initrd.systemd.initrdBin = [ cfg.package ];
+      boot.initrd.services.udev.binPackages = [ cfg.package ];
     })
     (mkIf cfg.dmeventd.enable {
       systemd.sockets."dm-event".wantedBy = [ "sockets.target" ];
diff --git a/nixos/modules/tasks/network-interfaces-scripted.nix b/nixos/modules/tasks/network-interfaces-scripted.nix
index f44dafc9706..24f0c37acf9 100644
--- a/nixos/modules/tasks/network-interfaces-scripted.nix
+++ b/nixos/modules/tasks/network-interfaces-scripted.nix
@@ -293,7 +293,7 @@ let
             script = ''
               # Remove Dead Interfaces
               echo "Removing old bridge ${n}..."
-              ip link show "${n}" >/dev/null 2>&1 && ip link del "${n}"
+              ip link show dev "${n}" >/dev/null 2>&1 && ip link del "${n}"
 
               echo "Adding bridge ${n}..."
               ip link add name "${n}" type bridge
@@ -396,7 +396,7 @@ let
             '';
             postStop = ''
               echo "Cleaning Open vSwitch ${n}"
-              echo "Shuting down internal ${n} interface"
+              echo "Shutting down internal ${n} interface"
               ip link set ${n} down || true
               echo "Deleting flows for ${n}"
               ovs-ofctl --protocols=${v.openFlowVersion} del-flows ${n} || true
@@ -459,7 +459,7 @@ let
             path = [ pkgs.iproute2 ];
             script = ''
               # Remove Dead Interfaces
-              ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
+              ip link show dev "${n}" >/dev/null 2>&1 && ip link delete "${n}"
               ip link add link "${v.interface}" name "${n}" type macvlan \
                 ${optionalString (v.mode != null) "mode ${v.mode}"}
               ip link set "${n}" up
@@ -517,7 +517,7 @@ let
             path = [ pkgs.iproute2 ];
             script = ''
               # Remove Dead Interfaces
-              ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
+              ip link show dev "${n}" >/dev/null 2>&1 && ip link delete "${n}"
               ip link add name "${n}" type sit \
                 ${optionalString (v.remote != null) "remote \"${v.remote}\""} \
                 ${optionalString (v.local != null) "local \"${v.local}\""} \
@@ -551,7 +551,7 @@ let
             path = [ pkgs.iproute2 ];
             script = ''
               # Remove Dead Interfaces
-              ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
+              ip link show dev "${n}" >/dev/null 2>&1 && ip link delete "${n}"
               ip link add name "${n}" type ${v.type} \
                 ${optionalString (v.remote != null) "remote \"${v.remote}\""} \
                 ${optionalString (v.local != null) "local \"${v.local}\""} \
@@ -579,7 +579,7 @@ let
             path = [ pkgs.iproute2 ];
             script = ''
               # Remove Dead Interfaces
-              ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
+              ip link show dev "${n}" >/dev/null 2>&1 && ip link delete "${n}"
               ip link add link "${v.interface}" name "${n}" type vlan id "${toString v.id}"
 
               # We try to bring up the logical VLAN interface. If the master
diff --git a/nixos/modules/tasks/network-interfaces-systemd.nix b/nixos/modules/tasks/network-interfaces-systemd.nix
index b24b29c32d4..dfa883a2c33 100644
--- a/nixos/modules/tasks/network-interfaces-systemd.nix
+++ b/nixos/modules/tasks/network-interfaces-systemd.nix
@@ -28,11 +28,164 @@ let
     # TODO: warn the user that any address configured on those interfaces will be useless
     ++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches);
 
+  domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain);
+  genericNetwork = override:
+    let gateway = optional (cfg.defaultGateway != null && (cfg.defaultGateway.address or "") != "") cfg.defaultGateway.address
+      ++ optional (cfg.defaultGateway6 != null && (cfg.defaultGateway6.address or "") != "") cfg.defaultGateway6.address;
+        makeGateway = gateway: {
+          routeConfig = {
+            Gateway = gateway;
+            GatewayOnLink = false;
+          };
+        };
+    in optionalAttrs (gateway != [ ]) {
+      routes = override (map makeGateway gateway);
+    } // optionalAttrs (domains != [ ]) {
+      domains = override domains;
+    };
+
+  genericDhcpNetworks = initrd: mkIf cfg.useDHCP {
+    networks."99-ethernet-default-dhcp" = {
+      # We want to match physical ethernet interfaces as commonly
+      # found on laptops, desktops and servers, to provide an
+      # "out-of-the-box" setup that works for common cases.  This
+      # heuristic isn't perfect (it could match interfaces with
+      # custom names that _happen_ to start with en or eth), but
+      # should be good enough to make the common case easy and can
+      # be overridden on a case-by-case basis using
+      # higher-priority networks or by disabling useDHCP.
+
+      # Type=ether matches veth interfaces as well, and this is
+      # more likely to result in interfaces being configured to
+      # use DHCP when they shouldn't.
+
+      # When wait-online.anyInterface is enabled, RequiredForOnline really
+      # means "sufficient for online", so we can enable it.
+      # Otherwise, don't block the network coming online because of default networks.
+      matchConfig.Name = ["en*" "eth*"];
+      DHCP = "yes";
+      linkConfig.RequiredForOnline =
+        lib.mkDefault (if initrd
+        then config.boot.initrd.systemd.network.wait-online.anyInterface
+        else config.systemd.network.wait-online.anyInterface);
+      networkConfig.IPv6PrivacyExtensions = "kernel";
+    };
+    networks."99-wireless-client-dhcp" = {
+      # Like above, but this is much more likely to be correct.
+      matchConfig.WLANInterfaceType = "station";
+      DHCP = "yes";
+      linkConfig.RequiredForOnline =
+        lib.mkDefault config.systemd.network.wait-online.anyInterface;
+      networkConfig.IPv6PrivacyExtensions = "kernel";
+      # We also set the route metric to one more than the default
+      # of 1024, so that Ethernet is preferred if both are
+      # available.
+      dhcpV4Config.RouteMetric = 1025;
+      ipv6AcceptRAConfig.RouteMetric = 1025;
+    };
+  };
+
+
+  interfaceNetworks = mkMerge (forEach interfaces (i: {
+    netdevs = mkIf i.virtual ({
+      "40-${i.name}" = {
+        netdevConfig = {
+          Name = i.name;
+          Kind = i.virtualType;
+        };
+        "${i.virtualType}Config" = optionalAttrs (i.virtualOwner != null) {
+          User = i.virtualOwner;
+        };
+      };
+    });
+    networks."40-${i.name}" = mkMerge [ (genericNetwork id) {
+      name = mkDefault i.name;
+      DHCP = mkForce (dhcpStr
+        (if i.useDHCP != null then i.useDHCP else false));
+      address = forEach (interfaceIps i)
+        (ip: "${ip.address}/${toString ip.prefixLength}");
+      routes = forEach (interfaceRoutes i)
+        (route: {
+          # Most of these route options have not been tested.
+          # Please fix or report any mistakes you may find.
+          routeConfig =
+            optionalAttrs (route.address != null && route.prefixLength != null) {
+              Destination = "${route.address}/${toString route.prefixLength}";
+            } //
+            optionalAttrs (route.options ? fastopen_no_cookie) {
+              FastOpenNoCookie = route.options.fastopen_no_cookie;
+            } //
+            optionalAttrs (route.via != null) {
+              Gateway = route.via;
+            } //
+            optionalAttrs (route.type != null) {
+              Type = route.type;
+            } //
+            optionalAttrs (route.options ? onlink) {
+              GatewayOnLink = true;
+            } //
+            optionalAttrs (route.options ? initrwnd) {
+              InitialAdvertisedReceiveWindow = route.options.initrwnd;
+            } //
+            optionalAttrs (route.options ? initcwnd) {
+              InitialCongestionWindow = route.options.initcwnd;
+            } //
+            optionalAttrs (route.options ? pref) {
+              IPv6Preference = route.options.pref;
+            } //
+            optionalAttrs (route.options ? mtu) {
+              MTUBytes = route.options.mtu;
+            } //
+            optionalAttrs (route.options ? metric) {
+              Metric = route.options.metric;
+            } //
+            optionalAttrs (route.options ? src) {
+              PreferredSource = route.options.src;
+            } //
+            optionalAttrs (route.options ? protocol) {
+              Protocol = route.options.protocol;
+            } //
+            optionalAttrs (route.options ? quickack) {
+              QuickAck = route.options.quickack;
+            } //
+            optionalAttrs (route.options ? scope) {
+              Scope = route.options.scope;
+            } //
+            optionalAttrs (route.options ? from) {
+              Source = route.options.from;
+            } //
+            optionalAttrs (route.options ? table) {
+              Table = route.options.table;
+            } //
+            optionalAttrs (route.options ? advmss) {
+              TCPAdvertisedMaximumSegmentSize = route.options.advmss;
+            } //
+            optionalAttrs (route.options ? ttl-propagate) {
+              TTLPropagate = route.options.ttl-propagate == "enabled";
+            };
+        });
+      networkConfig.IPv6PrivacyExtensions = "kernel";
+      linkConfig = optionalAttrs (i.macAddress != null) {
+        MACAddress = i.macAddress;
+      } // optionalAttrs (i.mtu != null) {
+        MTUBytes = toString i.mtu;
+      };
+    }];
+  }));
+
 in
 
 {
+  config = mkMerge [
 
-  config = mkIf cfg.useNetworkd {
+  (mkIf config.boot.initrd.network.enable {
+    # Note this is if initrd.network.enable, not if
+    # initrd.systemd.network.enable. By setting the latter and not the
+    # former, the user retains full control over the configuration.
+    boot.initrd.systemd.network = mkMerge [(genericDhcpNetworks true) interfaceNetworks];
+  })
+
+  (mkIf cfg.useNetworkd {
 
     assertions = [ {
       assertion = cfg.defaultGatewayWindowSize == null;
@@ -54,149 +207,11 @@ in
     networking.dhcpcd.enable = mkDefault false;
 
     systemd.network =
-      let
-        domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain);
-        genericNetwork = override:
-          let gateway = optional (cfg.defaultGateway != null && (cfg.defaultGateway.address or "") != "") cfg.defaultGateway.address
-            ++ optional (cfg.defaultGateway6 != null && (cfg.defaultGateway6.address or "") != "") cfg.defaultGateway6.address;
-              makeGateway = gateway: {
-                routeConfig = {
-                  Gateway = gateway;
-                  GatewayOnLink = false;
-                };
-              };
-          in optionalAttrs (gateway != [ ]) {
-            routes = override (map makeGateway gateway);
-          } // optionalAttrs (domains != [ ]) {
-            domains = override domains;
-          };
-      in mkMerge [ {
+      mkMerge [ {
         enable = true;
       }
-      (mkIf cfg.useDHCP {
-        networks."99-ethernet-default-dhcp" = lib.mkIf cfg.useDHCP {
-          # We want to match physical ethernet interfaces as commonly
-          # found on laptops, desktops and servers, to provide an
-          # "out-of-the-box" setup that works for common cases.  This
-          # heuristic isn't perfect (it could match interfaces with
-          # custom names that _happen_ to start with en or eth), but
-          # should be good enough to make the common case easy and can
-          # be overridden on a case-by-case basis using
-          # higher-priority networks or by disabling useDHCP.
-
-          # Type=ether matches veth interfaces as well, and this is
-          # more likely to result in interfaces being configured to
-          # use DHCP when they shouldn't.
-
-          # When wait-online.anyInterface is enabled, RequiredForOnline really
-          # means "sufficient for online", so we can enable it.
-          # Otherwise, don't block the network coming online because of default networks.
-          matchConfig.Name = ["en*" "eth*"];
-          DHCP = "yes";
-          linkConfig.RequiredForOnline =
-            lib.mkDefault config.systemd.network.wait-online.anyInterface;
-          networkConfig.IPv6PrivacyExtensions = "kernel";
-        };
-        networks."99-wireless-client-dhcp" = lib.mkIf cfg.useDHCP {
-          # Like above, but this is much more likely to be correct.
-          matchConfig.WLANInterfaceType = "station";
-          DHCP = "yes";
-          linkConfig.RequiredForOnline =
-            lib.mkDefault config.systemd.network.wait-online.anyInterface;
-          networkConfig.IPv6PrivacyExtensions = "kernel";
-          # We also set the route metric to one more than the default
-          # of 1024, so that Ethernet is preferred if both are
-          # available.
-          dhcpV4Config.RouteMetric = 1025;
-          ipv6AcceptRAConfig.RouteMetric = 1025;
-        };
-      })
-      (mkMerge (forEach interfaces (i: {
-        netdevs = mkIf i.virtual ({
-          "40-${i.name}" = {
-            netdevConfig = {
-              Name = i.name;
-              Kind = i.virtualType;
-            };
-            "${i.virtualType}Config" = optionalAttrs (i.virtualOwner != null) {
-              User = i.virtualOwner;
-            };
-          };
-        });
-        networks."40-${i.name}" = mkMerge [ (genericNetwork id) {
-          name = mkDefault i.name;
-          DHCP = mkForce (dhcpStr
-            (if i.useDHCP != null then i.useDHCP else false));
-          address = forEach (interfaceIps i)
-            (ip: "${ip.address}/${toString ip.prefixLength}");
-          routes = forEach (interfaceRoutes i)
-            (route: {
-              # Most of these route options have not been tested.
-              # Please fix or report any mistakes you may find.
-              routeConfig =
-                optionalAttrs (route.address != null && route.prefixLength != null) {
-                  Destination = "${route.address}/${toString route.prefixLength}";
-                } //
-                optionalAttrs (route.options ? fastopen_no_cookie) {
-                  FastOpenNoCookie = route.options.fastopen_no_cookie;
-                } //
-                optionalAttrs (route.via != null) {
-                  Gateway = route.via;
-                } //
-                optionalAttrs (route.type != null) {
-                  Type = route.type;
-                } //
-                optionalAttrs (route.options ? onlink) {
-                  GatewayOnLink = true;
-                } //
-                optionalAttrs (route.options ? initrwnd) {
-                  InitialAdvertisedReceiveWindow = route.options.initrwnd;
-                } //
-                optionalAttrs (route.options ? initcwnd) {
-                  InitialCongestionWindow = route.options.initcwnd;
-                } //
-                optionalAttrs (route.options ? pref) {
-                  IPv6Preference = route.options.pref;
-                } //
-                optionalAttrs (route.options ? mtu) {
-                  MTUBytes = route.options.mtu;
-                } //
-                optionalAttrs (route.options ? metric) {
-                  Metric = route.options.metric;
-                } //
-                optionalAttrs (route.options ? src) {
-                  PreferredSource = route.options.src;
-                } //
-                optionalAttrs (route.options ? protocol) {
-                  Protocol = route.options.protocol;
-                } //
-                optionalAttrs (route.options ? quickack) {
-                  QuickAck = route.options.quickack;
-                } //
-                optionalAttrs (route.options ? scope) {
-                  Scope = route.options.scope;
-                } //
-                optionalAttrs (route.options ? from) {
-                  Source = route.options.from;
-                } //
-                optionalAttrs (route.options ? table) {
-                  Table = route.options.table;
-                } //
-                optionalAttrs (route.options ? advmss) {
-                  TCPAdvertisedMaximumSegmentSize = route.options.advmss;
-                } //
-                optionalAttrs (route.options ? ttl-propagate) {
-                  TTLPropagate = route.options.ttl-propagate == "enabled";
-                };
-            });
-          networkConfig.IPv6PrivacyExtensions = "kernel";
-          linkConfig = optionalAttrs (i.macAddress != null) {
-            MACAddress = i.macAddress;
-          } // optionalAttrs (i.mtu != null) {
-            MTUBytes = toString i.mtu;
-          };
-        }];
-      })))
+      (genericDhcpNetworks false)
+      interfaceNetworks
       (mkMerge (flip mapAttrsToList cfg.bridges (name: bridge: {
         netdevs."40-${name}" = {
           netdevConfig = {
@@ -422,7 +437,7 @@ in
             '';
             postStop = ''
               echo "Cleaning Open vSwitch ${n}"
-              echo "Shuting down internal ${n} interface"
+              echo "Shutting down internal ${n} interface"
               ip link set ${n} down || true
               echo "Deleting flows for ${n}"
               ovs-ofctl --protocols=${v.openFlowVersion} del-flows ${n} || true
@@ -437,6 +452,7 @@ in
               bindsTo = [ "systemd-networkd.service" ];
           };
       };
-  };
+  })
 
+  ];
 }
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index 4d47a56ccca..eb1c7512d92 100644
--- a/nixos/modules/tasks/network-interfaces.nix
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -434,7 +434,8 @@ in
   options = {
 
     networking.hostName = mkOption {
-      default = "nixos";
+      default = config.system.nixos.distroId;
+      defaultText = literalExpression "config.system.nixos.distroId";
       # Only allow hostnames without the domain name part (i.e. no FQDNs, see
       # e.g. "man 5 hostname") and require valid DNS labels (recommended
       # syntax). Note: We also allow underscores for compatibility/legacy
@@ -1495,7 +1496,7 @@ in
           in
           ''
             # override to ${msg} for ${i.name}
-            ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.procps}/bin/sysctl net.ipv6.conf.${replaceStrings ["."] ["/"] i.name}.use_tempaddr=${val}"
+            ACTION=="add", SUBSYSTEM=="net", NAME=="${i.name}", RUN+="${pkgs.procps}/bin/sysctl net.ipv6.conf.${replaceStrings ["."] ["/"] i.name}.use_tempaddr=${val}"
           '') (filter (i: i.tempAddress != cfg.tempAddresses) interfaces);
       })
     ] ++ lib.optional (cfg.wlanInterfaces != {})
diff --git a/nixos/modules/tasks/swraid.nix b/nixos/modules/tasks/swraid.nix
index 7832bbf9201..1c3f1db1509 100644
--- a/nixos/modules/tasks/swraid.nix
+++ b/nixos/modules/tasks/swraid.nix
@@ -5,8 +5,12 @@
 in {
 
   options.boot.initrd.services.swraid = {
-    enable = (lib.mkEnableOption (lib.mdDoc "swraid support using mdadm")) // {
-      visible = false; # only has effect when the new stage 1 is in place
+    enable = lib.mkEnableOption (lib.mdDoc "swraid support using mdadm") // {
+      description = ''
+        *This will only be used when systemd is used in stage 1.*
+
+        Whether to enable swraid support using mdadm.
+      '';
     };
 
     mdadmConf = lib.mkOption {
diff --git a/nixos/modules/testing/test-instrumentation.nix b/nixos/modules/testing/test-instrumentation.nix
index 028099c6464..6dc4091bad1 100644
--- a/nixos/modules/testing/test-instrumentation.nix
+++ b/nixos/modules/testing/test-instrumentation.nix
@@ -36,8 +36,16 @@ in
             while ! exec 2> /dev/${qemu-common.qemuSerialDevice}; do sleep 0.1; done
             echo "connecting to host..." >&2
             stty -F /dev/hvc0 raw -echo # prevent nl -> cr/nl conversion
-            echo
-            PS1= exec /bin/sh
+            # The following line is essential since it signals to
+            # the test driver that the shell is ready.
+            # See: the connect method in the Machine class.
+            echo "Spawning backdoor root shell..."
+            # Passing the terminal device makes bash run non-interactively.
+            # Otherwise we get errors on the terminal because bash tries to
+            # setup things like job control.
+            # Note: calling bash explicitly here instead of sh makes sure that
+            # we can also run non-NixOS guests during tests.
+            PS1= exec /usr/bin/env bash --norc /dev/hvc0
           '';
         serviceConfig.KillSignal = "SIGHUP";
       };
@@ -107,10 +115,12 @@ in
       ShowStatus=no
       # Allow very slow start
       DefaultTimeoutStartSec=300
+      DefaultDeviceTimeoutSec=300
     '';
     systemd.user.extraConfig = ''
       # Allow very slow start
       DefaultTimeoutStartSec=300
+      DefaultDeviceTimeoutSec=300
     '';
 
     boot.initrd.systemd.extraConfig = config.systemd.extraConfig;
diff --git a/nixos/modules/virtualisation/amazon-ec2-amis.nix b/nixos/modules/virtualisation/amazon-ec2-amis.nix
index 446a1b0ecaa..ff88f02e5d3 100644
--- a/nixos/modules/virtualisation/amazon-ec2-amis.nix
+++ b/nixos/modules/virtualisation/amazon-ec2-amis.nix
@@ -536,5 +536,53 @@ let self = {
   "22.11".us-west-1.aarch64-linux.hvm-ebs = "ami-052d52b9e30a18562";
   "22.11".us-west-2.aarch64-linux.hvm-ebs = "ami-07418b6a4782c9521";
 
-  latest = self."22.11";
+  # 23.05.426.afc48694f2a
+
+  "23.05".eu-west-1.x86_64-linux.hvm-ebs = "ami-0fc7825fe890f87d1";
+  "23.05".af-south-1.x86_64-linux.hvm-ebs = "ami-0df2f7b42bfbd53e5";
+  "23.05".ap-east-1.x86_64-linux.hvm-ebs = "ami-07ba84d7321f6f4bb";
+  "23.05".ap-northeast-1.x86_64-linux.hvm-ebs = "ami-0e37827874573dbbf";
+  "23.05".ap-northeast-2.x86_64-linux.hvm-ebs = "ami-0ff5b3b7738651895";
+  "23.05".ap-northeast-3.x86_64-linux.hvm-ebs = "ami-0a7861571eb44c70c";
+  "23.05".ap-south-1.x86_64-linux.hvm-ebs = "ami-05c4802ca81d7c95b";
+  "23.05".ap-southeast-1.x86_64-linux.hvm-ebs = "ami-0aee8193da16bd2db";
+  "23.05".ap-southeast-2.x86_64-linux.hvm-ebs = "ami-008be032289f60d16";
+  "23.05".ap-southeast-3.x86_64-linux.hvm-ebs = "ami-033debde7c1659c96";
+  "23.05".ca-central-1.x86_64-linux.hvm-ebs = "ami-031821b5f83896474";
+  "23.05".eu-central-1.x86_64-linux.hvm-ebs = "ami-0d6ee9d5e1c985df6";
+  "23.05".eu-north-1.x86_64-linux.hvm-ebs = "ami-0cecb1f67b2a837f6";
+  "23.05".eu-south-1.x86_64-linux.hvm-ebs = "ami-0f9fee15eb5a64ac4";
+  "23.05".eu-west-2.x86_64-linux.hvm-ebs = "ami-0e62fef78d2c4f031";
+  "23.05".eu-west-3.x86_64-linux.hvm-ebs = "ami-01a6e4c1659b08390";
+  "23.05".me-south-1.x86_64-linux.hvm-ebs = "ami-0a01a7eeffa8f0fd5";
+  "23.05".sa-east-1.x86_64-linux.hvm-ebs = "ami-09a1760227f929ccf";
+  "23.05".us-east-1.x86_64-linux.hvm-ebs = "ami-07df5833f04703a2a";
+  "23.05".us-east-2.x86_64-linux.hvm-ebs = "ami-04dd2f100d9665df5";
+  "23.05".us-west-1.x86_64-linux.hvm-ebs = "ami-0fe502361fea4216c";
+  "23.05".us-west-2.x86_64-linux.hvm-ebs = "ami-0749963dd978a57c7";
+
+  "23.05".eu-west-1.aarch64-linux.hvm-ebs = "ami-0a0609421e5638005";
+  "23.05".af-south-1.aarch64-linux.hvm-ebs = "ami-05d95a055aba9373e";
+  "23.05".ap-east-1.aarch64-linux.hvm-ebs = "ami-08ae0190b1357465b";
+  "23.05".ap-northeast-1.aarch64-linux.hvm-ebs = "ami-09418b2049c3c9533";
+  "23.05".ap-northeast-2.aarch64-linux.hvm-ebs = "ami-040713ad23b404271";
+  "23.05".ap-northeast-3.aarch64-linux.hvm-ebs = "ami-0c888d6c1d989db68";
+  "23.05".ap-south-1.aarch64-linux.hvm-ebs = "ami-02da38deb21545675";
+  "23.05".ap-southeast-1.aarch64-linux.hvm-ebs = "ami-06df0713468bea276";
+  "23.05".ap-southeast-2.aarch64-linux.hvm-ebs = "ami-0171ee37ae5104c06";
+  "23.05".ap-southeast-3.aarch64-linux.hvm-ebs = "ami-075da61f5fef1fe80";
+  "23.05".ca-central-1.aarch64-linux.hvm-ebs = "ami-0ba8bd0a3d0a596f8";
+  "23.05".eu-central-1.aarch64-linux.hvm-ebs = "ami-0891608ae66031439";
+  "23.05".eu-north-1.aarch64-linux.hvm-ebs = "ami-0a3ad7ef18d595c68";
+  "23.05".eu-south-1.aarch64-linux.hvm-ebs = "ami-0fa86b680aa9a0444";
+  "23.05".eu-west-2.aarch64-linux.hvm-ebs = "ami-0a415791078f05970";
+  "23.05".eu-west-3.aarch64-linux.hvm-ebs = "ami-05d9b146317962e3b";
+  "23.05".me-south-1.aarch64-linux.hvm-ebs = "ami-0019b591acf30aa66";
+  "23.05".sa-east-1.aarch64-linux.hvm-ebs = "ami-030d6c30d91f06cc7";
+  "23.05".us-east-1.aarch64-linux.hvm-ebs = "ami-0a061ca437b63df33";
+  "23.05".us-east-2.aarch64-linux.hvm-ebs = "ami-0bf0b2b8fdfda30e8";
+  "23.05".us-west-1.aarch64-linux.hvm-ebs = "ami-0e75c8f3deb1f842b";
+  "23.05".us-west-2.aarch64-linux.hvm-ebs = "ami-0d0979d889078d036";
+
+  latest = self."23.05";
 }; in self
diff --git a/nixos/modules/virtualisation/amazon-image.nix b/nixos/modules/virtualisation/amazon-image.nix
index 9751f5755f9..aa44f264269 100644
--- a/nixos/modules/virtualisation/amazon-image.nix
+++ b/nixos/modules/virtualisation/amazon-image.nix
@@ -25,11 +25,7 @@ in
 
   config = {
 
-    assertions = [
-      { assertion = versionOlder config.boot.kernelPackages.kernel.version "5.17";
-        message = "ENA driver fails to build with kernel >= 5.17";
-      }
-    ];
+    assertions = [ ];
 
     boot.growPartition = true;
 
@@ -85,7 +81,7 @@ in
     # Allow root logins only using the SSH key that the user specified
     # at instance creation time.
     services.openssh.enable = true;
-    services.openssh.permitRootLogin = "prohibit-password";
+    services.openssh.settings.PermitRootLogin = "prohibit-password";
 
     # Enable the serial console on ttyS0
     systemd.services."serial-getty@ttyS0".enable = true;
diff --git a/nixos/modules/virtualisation/amazon-options.nix b/nixos/modules/virtualisation/amazon-options.nix
index 915bbf9763d..3ea4a6cf781 100644
--- a/nixos/modules/virtualisation/amazon-options.nix
+++ b/nixos/modules/virtualisation/amazon-options.nix
@@ -2,9 +2,6 @@
 let
   inherit (lib) literalExpression types;
 in {
-  imports = [
-    (lib.mkRemovedOptionModule [ "ec2" "hvm" ] "Only HVM instances are supported, so specifying it is no longer necessary.")
-  ];
   options = {
     ec2 = {
       zfs = {
@@ -31,13 +28,13 @@ in {
             options = {
               mount = lib.mkOption {
                 description = lib.mdDoc "Where to mount this dataset.";
-                type = types.nullOr types.string;
+                type = types.nullOr types.str;
                 default = null;
               };
 
               properties = lib.mkOption {
                 description = lib.mdDoc "Properties to set on this dataset.";
-                type = types.attrsOf types.string;
+                type = types.attrsOf types.str;
                 default = {};
               };
             };
@@ -52,6 +49,12 @@ in {
           Whether the EC2 instance is using EFI.
         '';
       };
+      hvm = lib.mkOption {
+        description = "Unused legacy option. While support for non-hvm has been dropped, we keep this option around so that NixOps remains compatible with a somewhat recent `nixpkgs` and machines with an old `stateVersion`.";
+        internal = true;
+        default = true;
+        readOnly = true;
+      };
     };
   };
 
diff --git a/nixos/modules/virtualisation/azure-agent-entropy.patch b/nixos/modules/virtualisation/azure-agent-entropy.patch
deleted file mode 100644
index 2a7ad08a4af..00000000000
--- a/nixos/modules/virtualisation/azure-agent-entropy.patch
+++ /dev/null
@@ -1,17 +0,0 @@
---- a/waagent	2016-03-12 09:58:15.728088851 +0200
-+++ a/waagent	2016-03-12 09:58:43.572680025 +0200
-@@ -6173,10 +6173,10 @@
-             Log("MAC  address: " + ":".join(["%02X" % Ord(a) for a in mac]))
-         
-         # Consume Entropy in ACPI table provided by Hyper-V
--        try:
--            SetFileContents("/dev/random", GetFileContents("/sys/firmware/acpi/tables/OEM0"))
--        except:
--            pass
-+        #try:
-+        #    SetFileContents("/dev/random", GetFileContents("/sys/firmware/acpi/tables/OEM0"))
-+        #except:
-+        #    pass
- 
-         Log("Probing for Azure environment.")
-         self.Endpoint = self.DoDhcpWork()
diff --git a/nixos/modules/virtualisation/azure-agent.nix b/nixos/modules/virtualisation/azure-agent.nix
index abe6455a1a6..6e6021cf80f 100644
--- a/nixos/modules/virtualisation/azure-agent.nix
+++ b/nixos/modules/virtualisation/azure-agent.nix
@@ -1,51 +1,10 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
 
   cfg = config.virtualisation.azure.agent;
 
-  waagent = with pkgs; stdenv.mkDerivation rec {
-    name = "waagent-2.0";
-    src = pkgs.fetchFromGitHub {
-      owner = "Azure";
-      repo = "WALinuxAgent";
-      rev = "1b3a8407a95344d9d12a2a377f64140975f1e8e4";
-      sha256 = "10byzvmpgrmr4d5mdn2kq04aapqb3sgr1admk13wjmy5cd6bwd2x";
-    };
-
-    patches = [ ./azure-agent-entropy.patch ];
-
-    nativeBuildInputs = [ makeWrapper python pythonPackages.wrapPython ];
-    runtimeDeps = [ findutils gnugrep gawk coreutils openssl openssh
-                    nettools # for hostname
-                    procps # for pidof
-                    shadow # for useradd, usermod
-                    util-linux # for (u)mount, fdisk, sfdisk, mkswap
-                    parted
-                  ];
-    pythonPath = [ pythonPackages.pyasn1 ];
-
-    configurePhase = false;
-    buildPhase = false;
-
-    installPhase = ''
-      substituteInPlace config/99-azure-product-uuid.rules \
-          --replace /bin/chmod "${coreutils}/bin/chmod"
-      mkdir -p $out/lib/udev/rules.d
-      cp config/*.rules $out/lib/udev/rules.d
-
-      mkdir -p $out/bin
-      cp waagent $out/bin/
-      chmod +x $out/bin/waagent
-
-      wrapProgram "$out/bin/waagent" \
-          --prefix PYTHONPATH : $PYTHONPATH \
-          --prefix PATH : "${makeBinPath runtimeDeps}"
-    '';
-  };
-
   provisionedHook = pkgs.writeScript "provisioned-hook" ''
     #!${pkgs.runtimeShell}
     /run/current-system/systemd/bin/systemctl start provisioned.target
@@ -74,14 +33,15 @@ in
 
   ###### implementation
 
-  config = mkIf cfg.enable {
-    assertions = [ {
+  config = lib.mkIf cfg.enable {
+    assertions = [{
       assertion = pkgs.stdenv.hostPlatform.isx86;
       message = "Azure not currently supported on ${pkgs.stdenv.hostPlatform.system}";
-    } {
-      assertion = config.networking.networkmanager.enable == false;
-      message = "Windows Azure Linux Agent is not compatible with NetworkManager";
-    } ];
+    }
+      {
+        assertion = config.networking.networkmanager.enable == false;
+        message = "Windows Azure Linux Agent is not compatible with NetworkManager";
+      }];
 
     boot.initrd.kernelModules = [ "ata_piix" ];
     networking.firewall.allowedUDPPorts = [ 68 ];
@@ -89,13 +49,19 @@ in
 
     environment.etc."waagent.conf".text = ''
         #
-        # Windows Azure Linux Agent Configuration
+        # Microsoft Azure Linux Agent Configuration
         #
 
-        Role.StateConsumer=${provisionedHook}
+        # Enable extension handling. Do not disable this unless you do not need password reset,
+        # backup, monitoring, or any extension handling whatsoever.
+        Extensions.Enabled=y
 
-        # Enable instance creation
-        Provisioning.Enabled=y
+        # How often (in seconds) to poll for new goal states
+        Extensions.GoalStatePeriod=6
+
+        # Which provisioning agent to use. Supported values are "auto" (default), "waagent",
+        # "cloud-init", or "disabled".
+        Provisioning.Agent=disabled
 
         # Password authentication for root account will be unavailable.
         Provisioning.DeleteRootPassword=n
@@ -103,18 +69,31 @@ in
         # Generate fresh host key pair.
         Provisioning.RegenerateSshHostKeyPair=n
 
-        # Supported values are "rsa", "dsa" and "ecdsa".
+        # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto".
+        # The "auto" option is supported on OpenSSH 5.9 (2011) and later.
         Provisioning.SshHostKeyPairType=ed25519
 
         # Monitor host name changes and publish changes via DHCP requests.
         Provisioning.MonitorHostName=y
 
+        # How often (in seconds) to monitor host name changes.
+        Provisioning.MonitorHostNamePeriod=30
+
         # Decode CustomData from Base64.
         Provisioning.DecodeCustomData=n
 
         # Execute CustomData after provisioning.
         Provisioning.ExecuteCustomData=n
 
+        # Algorithm used by crypt when generating password hash.
+        #Provisioning.PasswordCryptId=6
+
+        # Length of random salt used when generating password hash.
+        #Provisioning.PasswordCryptSaltLength=10
+
+        # Allow reset password of sys user
+        Provisioning.AllowResetSysUser=n
+
         # Format if unformatted. If 'n', resource disk will not be mounted.
         ResourceDisk.Format=${if cfg.mountResourceDisk then "y" else "n"}
 
@@ -125,22 +104,103 @@ in
         # Mount point for the resource disk
         ResourceDisk.MountPoint=/mnt/resource
 
-        # Respond to load balancer probes if requested by Windows Azure.
-        LBProbeResponder=y
+        # Create and use swapfile on resource disk.
+        ResourceDisk.EnableSwap=n
+
+        # Size of the swapfile.
+        ResourceDisk.SwapSizeMB=0
 
-        # Enable logging to serial console (y|n)
-        # When stdout is not enough...
-        # 'y' if not set
-        Logs.Console=y
+        # Comma-separated list of mount options. See mount(8) for valid options.
+        ResourceDisk.MountOptions=None
 
         # Enable verbose logging (y|n)
         Logs.Verbose=${if cfg.verboseLogging then "y" else "n"}
 
+        # Enable Console logging, default is y
+        # Logs.Console=y
+
+        # Enable periodic log collection, default is n
+        Logs.Collect=n
+
+        # How frequently to collect logs, default is each hour
+        Logs.CollectPeriod=3600
+
+        # Is FIPS enabled
+        OS.EnableFIPS=n
+
         # Root device timeout in seconds.
         OS.RootDeviceScsiTimeout=300
+
+        # How often (in seconds) to set the root device timeout.
+        OS.RootDeviceScsiTimeoutPeriod=30
+
+        # If "None", the system default version is used.
+        OS.OpensslPath=${pkgs.openssl_3.bin}/bin/openssl
+
+        # Set the SSH ClientAliveInterval
+        # OS.SshClientAliveInterval=180
+
+        # Set the path to SSH keys and configuration files
+        OS.SshDir=/etc/ssh
+
+        # If set, agent will use proxy server to access internet
+        #HttpProxy.Host=None
+        #HttpProxy.Port=None
+
+        # Detect Scvmm environment, default is n
+        # DetectScvmmEnv=n
+
+        #
+        # Lib.Dir=/var/lib/waagent
+
+        #
+        # DVD.MountPoint=/mnt/cdrom/secure
+
+        #
+        # Pid.File=/var/run/waagent.pid
+
+        #
+        # Extension.LogDir=/var/log/azure
+
+        #
+        # Home.Dir=/home
+
+        # Enable RDMA management and set up, should only be used in HPC images
+        OS.EnableRDMA=n
+
+        # Enable checking RDMA driver version and update
+        # OS.CheckRdmaDriver=y
+
+        # Enable or disable goal state processing auto-update, default is enabled
+        AutoUpdate.Enabled=n
+
+        # Determine the update family, this should not be changed
+        # AutoUpdate.GAFamily=Prod
+
+        # Determine if the overprovisioning feature is enabled. If yes, hold extension
+        # handling until inVMArtifactsProfile.OnHold is false.
+        # Default is enabled
+        EnableOverProvisioning=n
+
+        # Allow fallback to HTTP if HTTPS is unavailable
+        # Note: Allowing HTTP (vs. HTTPS) may cause security risks
+        # OS.AllowHTTP=n
+
+        # Add firewall rules to protect access to Azure host node services
+        OS.EnableFirewall=n
+
+        # How often (in seconds) to check the firewall rules
+        OS.EnableFirewallPeriod=30
+
+        # How often (in seconds) to remove the udev rules for persistent network interface
+        # names (75-persistent-net-generator.rules and /etc/udev/rules.d/70-persistent-net.rules)
+        OS.RemovePersistentNetRulesPeriod=30
+
+        # How often (in seconds) to monitor for DHCP client restarts
+        OS.MonitorDhcpClientRestartPeriod=30
     '';
 
-    services.udev.packages = [ waagent ];
+    services.udev.packages = [ pkgs.waagent ];
 
     networking.dhcpcd.persistent = true;
 
@@ -157,23 +217,24 @@ in
       description = "Services Requiring Azure VM provisioning to have finished";
     };
 
-  systemd.services.consume-hypervisor-entropy =
-    { description = "Consume entropy in ACPI table provided by Hyper-V";
-
-      wantedBy = [ "sshd.service" "waagent.service" ];
-      before = [ "sshd.service" "waagent.service" ];
-
-      path  = [ pkgs.coreutils ];
-      script =
-        ''
-          echo "Fetching entropy..."
-          cat /sys/firmware/acpi/tables/OEM0 > /dev/random
-        '';
-      serviceConfig.Type = "oneshot";
-      serviceConfig.RemainAfterExit = true;
-      serviceConfig.StandardError = "journal+console";
-      serviceConfig.StandardOutput = "journal+console";
-     };
+    systemd.services.consume-hypervisor-entropy =
+      {
+        description = "Consume entropy in ACPI table provided by Hyper-V";
+
+        wantedBy = [ "sshd.service" "waagent.service" ];
+        before = [ "sshd.service" "waagent.service" ];
+
+        path = [ pkgs.coreutils ];
+        script =
+          ''
+            echo "Fetching entropy..."
+            cat /sys/firmware/acpi/tables/OEM0 > /dev/random
+          '';
+        serviceConfig.Type = "oneshot";
+        serviceConfig.RemainAfterExit = true;
+        serviceConfig.StandardError = "journal+console";
+        serviceConfig.StandardOutput = "journal+console";
+      };
 
     systemd.services.waagent = {
       wantedBy = [ "multi-user.target" ];
@@ -184,11 +245,10 @@ in
       description = "Windows Azure Agent Service";
       unitConfig.ConditionPathExists = "/etc/waagent.conf";
       serviceConfig = {
-        ExecStart = "${waagent}/bin/waagent -daemon";
+        ExecStart = "${pkgs.waagent}/bin/waagent -daemon";
         Type = "simple";
       };
     };
 
   };
-
 }
diff --git a/nixos/modules/virtualisation/azure-common.nix b/nixos/modules/virtualisation/azure-common.nix
index dc7853b9503..cd1ffdb6cbc 100644
--- a/nixos/modules/virtualisation/azure-common.nix
+++ b/nixos/modules/virtualisation/azure-common.nix
@@ -12,7 +12,6 @@ with lib;
 
   # Generate a GRUB menu.
   boot.loader.grub.device = "/dev/sda";
-  boot.loader.grub.version = 2;
   boot.loader.timeout = 0;
 
   boot.growPartition = true;
@@ -30,10 +29,8 @@ with lib;
   # Allow root logins only using the SSH key that the user specified
   # at instance creation time, ping client connections to avoid timeouts
   services.openssh.enable = true;
-  services.openssh.permitRootLogin = "prohibit-password";
-  services.openssh.extraConfig = ''
-    ClientAliveInterval 180
-  '';
+  services.openssh.settings.PermitRootLogin = "prohibit-password";
+  services.openssh.settings.ClientAliveInterval = 180;
 
   # Force getting the hostname from Azure
   networking.hostName = mkDefault "";
diff --git a/nixos/modules/virtualisation/brightbox-image.nix b/nixos/modules/virtualisation/brightbox-image.nix
index 9641b693f18..15f8fd6d8f7 100644
--- a/nixos/modules/virtualisation/brightbox-image.nix
+++ b/nixos/modules/virtualisation/brightbox-image.nix
@@ -103,7 +103,7 @@ in
   # Allow root logins only using the SSH key that the user specified
   # at instance creation time.
   services.openssh.enable = true;
-  services.openssh.permitRootLogin = "prohibit-password";
+  services.openssh.settings.PermitRootLogin = "prohibit-password";
 
   # Force getting the hostname from Google Compute.
   networking.hostName = mkDefault "";
diff --git a/nixos/modules/virtualisation/cloudstack-config.nix b/nixos/modules/virtualisation/cloudstack-config.nix
index 78afebdc5dd..7df3c9c613b 100644
--- a/nixos/modules/virtualisation/cloudstack-config.nix
+++ b/nixos/modules/virtualisation/cloudstack-config.nix
@@ -21,7 +21,7 @@ with lib;
     # Allow root logins
     services.openssh = {
       enable = true;
-      permitRootLogin = "prohibit-password";
+      settings.PermitRootLogin = "prohibit-password";
     };
 
     # Cloud-init configuration.
diff --git a/nixos/modules/virtualisation/containers.nix b/nixos/modules/virtualisation/containers.nix
index fb9c19d79c1..3e33cabf266 100644
--- a/nixos/modules/virtualisation/containers.nix
+++ b/nixos/modules/virtualisation/containers.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, utils, ... }:
+{ config, lib, pkgs, ... }:
 let
   cfg = config.virtualisation.containers;
 
@@ -136,7 +136,7 @@ in
 
     environment.etc."containers/policy.json".source =
       if cfg.policy != { } then pkgs.writeText "policy.json" (builtins.toJSON cfg.policy)
-      else utils.copyFile "${pkgs.skopeo.src}/default-policy.json";
+      else "${pkgs.skopeo.policy}/default-policy.json";
   };
 
 }
diff --git a/nixos/modules/virtualisation/cri-o.nix b/nixos/modules/virtualisation/cri-o.nix
index 95ce1fea58b..dacd700537c 100644
--- a/nixos/modules/virtualisation/cri-o.nix
+++ b/nixos/modules/virtualisation/cri-o.nix
@@ -1,10 +1,13 @@
-{ config, lib, pkgs, utils, ... }:
+{ config, lib, pkgs, ... }:
 
 with lib;
 let
   cfg = config.virtualisation.cri-o;
 
-  crioPackage = (pkgs.cri-o.override { inherit (cfg) extraPackages; });
+  crioPackage = pkgs.cri-o.override {
+    extraPackages = cfg.extraPackages
+      ++ lib.optional (builtins.elem "zfs" config.boot.supportedFilesystems) config.boot.zfs.package;
+  };
 
   format = pkgs.formats.toml { };
 
@@ -19,7 +22,7 @@ in
     enable = mkEnableOption (lib.mdDoc "Container Runtime Interface for OCI (CRI-O)");
 
     storageDriver = mkOption {
-      type = types.enum [ "btrfs" "overlay" "vfs" ];
+      type = types.enum [ "aufs" "btrfs" "devmapper" "overlay" "vfs" "zfs" ];
       default = "overlay";
       description = lib.mdDoc "Storage driver to be used";
     };
@@ -93,7 +96,7 @@ in
   config = mkIf cfg.enable {
     environment.systemPackages = [ cfg.package pkgs.cri-tools ];
 
-    environment.etc."crictl.yaml".source = utils.copyFile "${pkgs.cri-o-unwrapped.src}/crictl.yaml";
+    environment.etc."crictl.yaml".source = "${cfg.package}/etc/crictl.yaml";
 
     virtualisation.cri-o.settings.crio = {
       storage_driver = cfg.storageDriver;
@@ -124,8 +127,8 @@ in
       };
     };
 
-    environment.etc."cni/net.d/10-crio-bridge.conf".source = utils.copyFile "${pkgs.cri-o-unwrapped.src}/contrib/cni/10-crio-bridge.conf";
-    environment.etc."cni/net.d/99-loopback.conf".source = utils.copyFile "${pkgs.cri-o-unwrapped.src}/contrib/cni/99-loopback.conf";
+    environment.etc."cni/net.d/10-crio-bridge.conflist".source = "${cfg.package}/etc/cni/net.d/10-crio-bridge.conflist";
+    environment.etc."cni/net.d/99-loopback.conflist".source = "${cfg.package}/etc/cni/net.d/99-loopback.conflist";
     environment.etc."crio/crio.conf.d/00-default.conf".source = cfgFile;
 
     # Enable common /etc/containers configuration
diff --git a/nixos/modules/virtualisation/digital-ocean-config.nix b/nixos/modules/virtualisation/digital-ocean-config.nix
index 754bc1a5185..e004b7880aa 100644
--- a/nixos/modules/virtualisation/digital-ocean-config.nix
+++ b/nixos/modules/virtualisation/digital-ocean-config.nix
@@ -49,7 +49,7 @@ with lib;
       };
       services.openssh = {
         enable = mkDefault true;
-        passwordAuthentication = mkDefault false;
+        settings.PasswordAuthentication = mkDefault false;
       };
       services.do-agent.enable = mkDefault true;
       networking = {
diff --git a/nixos/modules/virtualisation/docker.nix b/nixos/modules/virtualisation/docker.nix
index d9bd10ba1fc..046b8e2f790 100644
--- a/nixos/modules/virtualisation/docker.nix
+++ b/nixos/modules/virtualisation/docker.nix
@@ -100,7 +100,7 @@ in
 
     logDriver =
       mkOption {
-        type = types.enum ["none" "json-file" "syslog" "journald" "gelf" "fluentd" "awslogs" "splunk" "etwlogs" "gcplogs"];
+        type = types.enum ["none" "json-file" "syslog" "journald" "gelf" "fluentd" "awslogs" "splunk" "etwlogs" "gcplogs" "local"];
         default = "journald";
         description =
           lib.mdDoc ''
@@ -163,7 +163,7 @@ in
   ###### implementation
 
   config = mkIf cfg.enable (mkMerge [{
-      boot.kernelModules = [ "bridge" "veth" ];
+      boot.kernelModules = [ "bridge" "veth" "br_netfilter" "xt_nat" ];
       boot.kernel.sysctl = {
         "net.ipv4.conf.all.forwarding" = mkOverride 98 true;
         "net.ipv4.conf.default.forwarding" = mkOverride 98 true;
diff --git a/nixos/modules/virtualisation/ec2-metadata-fetcher.sh b/nixos/modules/virtualisation/ec2-metadata-fetcher.sh
index 9e204d45dbd..716aff7c22f 100644
--- a/nixos/modules/virtualisation/ec2-metadata-fetcher.sh
+++ b/nixos/modules/virtualisation/ec2-metadata-fetcher.sh
@@ -55,10 +55,9 @@ done
 echo "getting EC2 instance metadata..."
 
 get_imds() {
-  # Intentionally no --fail here, so that we proceed even if e.g. a
-  # 404 was returned (but we still fail if we can't reach the IMDS
-  # server).
-  curl --silent --show-error --header "X-aws-ec2-metadata-token: $IMDS_TOKEN" "$@"
+  # --fail to avoid populating missing files with 404 HTML response body
+  # || true to allow the script to continue even when encountering a 404
+  curl --silent --show-error --fail --header "X-aws-ec2-metadata-token: $IMDS_TOKEN" "$@" || true
 }
 
 get_imds -o "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
diff --git a/nixos/modules/virtualisation/google-compute-config.nix b/nixos/modules/virtualisation/google-compute-config.nix
index 44d2a589511..cf94ce0faf3 100644
--- a/nixos/modules/virtualisation/google-compute-config.nix
+++ b/nixos/modules/virtualisation/google-compute-config.nix
@@ -1,5 +1,15 @@
 { config, lib, pkgs, ... }:
-with lib;
+
+let
+  inherit (lib)
+    boolToString
+    mkDefault
+    mkIf
+    optional
+    readFile
+  ;
+in
+
 {
   imports = [
     ../profiles/headless.nix
@@ -29,8 +39,8 @@ with lib;
   # Allow root logins only using SSH keys
   # and disable password authentication in general
   services.openssh.enable = true;
-  services.openssh.permitRootLogin = "prohibit-password";
-  services.openssh.passwordAuthentication = mkDefault false;
+  services.openssh.settings.PermitRootLogin = "prohibit-password";
+  services.openssh.settings.PasswordAuthentication = mkDefault false;
 
   # enable OS Login. This also requires setting enable-oslogin=TRUE metadata on
   # instance or project level
@@ -65,7 +75,7 @@ with lib;
   systemd.services.google-guest-agent = {
     wantedBy = [ "multi-user.target" ];
     restartTriggers = [ config.environment.etc."default/instance_configs.cfg".source ];
-    path = lib.optional config.users.mutableUsers pkgs.shadow;
+    path = optional config.users.mutableUsers pkgs.shadow;
   };
   systemd.services.google-startup-scripts.wantedBy = [ "multi-user.target" ];
   systemd.services.google-shutdown-scripts.wantedBy = [ "multi-user.target" ];
@@ -76,7 +86,7 @@ with lib;
 
   users.groups.google-sudoers = mkIf config.users.mutableUsers { };
 
-  boot.extraModprobeConfig = lib.readFile "${pkgs.google-guest-configs}/etc/modprobe.d/gce-blacklist.conf";
+  boot.extraModprobeConfig = readFile "${pkgs.google-guest-configs}/etc/modprobe.d/gce-blacklist.conf";
 
   environment.etc."sysctl.d/60-gce-network-security.conf".source = "${pkgs.google-guest-configs}/etc/sysctl.d/60-gce-network-security.conf";
 
diff --git a/nixos/modules/virtualisation/libvirtd.nix b/nixos/modules/virtualisation/libvirtd.nix
index 7c95405ed31..8dfe04cea54 100644
--- a/nixos/modules/virtualisation/libvirtd.nix
+++ b/nixos/modules/virtualisation/libvirtd.nix
@@ -308,7 +308,9 @@ in
             libvirt/nwfilter/*.xml );
         do
             mkdir -p /var/lib/$(dirname $i) -m 755
-            cp -npd ${cfg.package}/var/lib/$i /var/lib/$i
+            if [ ! -e /var/lib/$i ]; then
+              cp -pd ${cfg.package}/var/lib/$i /var/lib/$i
+            fi
         done
 
         # Copy generated qemu config to libvirt directory
@@ -414,13 +416,16 @@ in
     # https://libvirt.org/daemons.html#monolithic-systemd-integration
     systemd.sockets.libvirtd.wantedBy = [ "sockets.target" ];
 
-    security.polkit.extraConfig = ''
-      polkit.addRule(function(action, subject) {
-        if (action.id == "org.libvirt.unix.manage" &&
-          subject.isInGroup("libvirtd")) {
-          return polkit.Result.YES;
-        }
-      });
-    '';
+    security.polkit = {
+      enable = true;
+      extraConfig = ''
+        polkit.addRule(function(action, subject) {
+          if (action.id == "org.libvirt.unix.manage" &&
+            subject.isInGroup("libvirtd")) {
+            return polkit.Result.YES;
+          }
+        });
+      '';
+    };
   };
 }
diff --git a/nixos/modules/virtualisation/linode-config.nix b/nixos/modules/virtualisation/linode-config.nix
index d664e8269f4..bbf81bda9c0 100644
--- a/nixos/modules/virtualisation/linode-config.nix
+++ b/nixos/modules/virtualisation/linode-config.nix
@@ -6,8 +6,8 @@ with lib;
   services.openssh = {
     enable = true;
 
-    permitRootLogin = "prohibit-password";
-    passwordAuthentication = mkDefault false;
+    settings.PermitRootLogin = "prohibit-password";
+    settings.PasswordAuthentication = mkDefault false;
   };
 
   networking = {
diff --git a/nixos/modules/virtualisation/linode-image.nix b/nixos/modules/virtualisation/linode-image.nix
index f8d212d9cda..51f793ac011 100644
--- a/nixos/modules/virtualisation/linode-image.nix
+++ b/nixos/modules/virtualisation/linode-image.nix
@@ -62,5 +62,5 @@ in
     };
   };
 
-  meta.maintainers = with maintainers; [ houstdav000 ];
+  meta.maintainers = with maintainers; [ cyntheticfox ];
 }
diff --git a/nixos/modules/virtualisation/lxc-container.nix b/nixos/modules/virtualisation/lxc-container.nix
index 4963d9f3f9e..55b285b6914 100644
--- a/nixos/modules/virtualisation/lxc-container.nix
+++ b/nixos/modules/virtualisation/lxc-container.nix
@@ -123,8 +123,8 @@ in
             architecture = builtins.elemAt (builtins.match "^([a-z0-9_]+).+" (toString pkgs.system)) 0;
             creation_date = 1;
             properties = {
-              description = "NixOS ${config.system.nixos.codeName} ${config.system.nixos.label} ${pkgs.system}";
-              os = "nixos";
+              description = "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} ${pkgs.system}";
+              os = "${config.system.nixos.distroId}";
               release = "${config.system.nixos.codeName}";
             };
             templates = templates.properties;
@@ -150,11 +150,22 @@ in
           source = config.system.build.toplevel + "/init";
           target = "/sbin/init";
         }
+        # Technically this is not required for lxc, but having also make this configuration work with systemd-nspawn.
+        # Nixos will setup the same symlink after start.
+        {
+          source = config.system.build.toplevel + "/etc/os-release";
+          target = "/etc/os-release";
+        }
       ];
 
       extraCommands = "mkdir -p proc sys dev";
     };
 
+    system.build.installBootLoader = pkgs.writeScript "install-lxd-sbin-init.sh" ''
+      #!${pkgs.runtimeShell}
+      ln -fs "$1/init" /sbin/init
+    '';
+
     # Add the overrides from lxd distrobuilder
     # https://github.com/lxc/distrobuilder/blob/05978d0d5a72718154f1525c7d043e090ba7c3e0/distrobuilder/main.go#L630
     systemd.packages = [
diff --git a/nixos/modules/virtualisation/lxd.nix b/nixos/modules/virtualisation/lxd.nix
index c06716e5eb6..22e336c895f 100644
--- a/nixos/modules/virtualisation/lxd.nix
+++ b/nixos/modules/virtualisation/lxd.nix
@@ -85,6 +85,14 @@ in {
           considered failed and systemd will attempt to restart it.
         '';
       };
+
+      ui = {
+        enable = lib.mkEnableOption (lib.mdDoc ''
+          Enables the (experimental) LXD UI.
+        '');
+
+        package = mkPackageOption pkgs.lxd "ui" { };
+      };
     };
   };
 
@@ -143,6 +151,10 @@ in {
       path = [ pkgs.util-linux ]
         ++ optional cfg.zfsSupport config.boot.zfs.package;
 
+      environment = mkIf (cfg.ui.enable) {
+        "LXD_UI" = cfg.ui.package;
+      };
+
       serviceConfig = {
         ExecStart = "@${cfg.package}/bin/lxd lxd --group lxd";
         ExecStartPost = "${cfg.package}/bin/lxd waitready --timeout=${cfg.startTimeout}";
@@ -177,7 +189,7 @@ in {
       "fs.inotify.max_queued_events" = 1048576;
       "fs.inotify.max_user_instances" = 1048576;
       "fs.inotify.max_user_watches" = 1048576;
-      "vm.max_map_count" = 262144;
+      "vm.max_map_count" = 262144; # TODO: Default vm.max_map_count has been increased system-wide
       "kernel.dmesg_restrict" = 1;
       "net.ipv4.neigh.default.gc_thresh3" = 8192;
       "net.ipv6.neigh.default.gc_thresh3" = 8192;
diff --git a/nixos/modules/virtualisation/multipass.nix b/nixos/modules/virtualisation/multipass.nix
new file mode 100644
index 00000000000..b331b3be7ea
--- /dev/null
+++ b/nixos/modules/virtualisation/multipass.nix
@@ -0,0 +1,61 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.virtualisation.multipass;
+in
+{
+  options = {
+    virtualisation.multipass = {
+      enable = lib.mkEnableOption (lib.mdDoc ''
+        Multipass, a simple manager for virtualised Ubuntu instances.
+      '');
+
+      logLevel = lib.mkOption {
+        type = lib.types.enum [ "error" "warning" "info" "debug" "trace" ];
+        default = "debug";
+        description = lib.mdDoc ''
+          The logging verbosity of the multipassd binary.
+        '';
+      };
+
+      package = lib.mkPackageOptionMD pkgs "multipass" { };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.multipass = {
+      description = "Multipass orchestrates virtual Ubuntu instances.";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+
+      environment = {
+        "XDG_DATA_HOME" = "/var/lib/multipass/data";
+        "XDG_CACHE_HOME" = "/var/lib/multipass/cache";
+        "XDG_CONFIG_HOME" = "/var/lib/multipass/config";
+      };
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/multipassd --logger platform --verbosity ${cfg.logLevel}";
+        SyslogIdentifier = "multipassd";
+        Restart = "on-failure";
+        TimeoutStopSec = 300;
+        Type = "simple";
+
+        WorkingDirectory = "/var/lib/multipass";
+
+        StateDirectory = "multipass";
+        StateDirectoryMode = "0750";
+        CacheDirectory = "multipass";
+        CacheDirectoryMode = "0750";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/nixos-containers.nix b/nixos/modules/virtualisation/nixos-containers.nix
index e1e640c4474..5df9942dbc0 100644
--- a/nixos/modules/virtualisation/nixos-containers.nix
+++ b/nixos/modules/virtualisation/nixos-containers.nix
@@ -170,11 +170,11 @@ let
         --setenv HOST_PORT="$HOST_PORT" \
         --setenv PATH="$PATH" \
         ${optionalString cfg.ephemeral "--ephemeral"} \
-        ${if cfg.additionalCapabilities != null && cfg.additionalCapabilities != [] then
-          ''--capability="${concatStringsSep "," cfg.additionalCapabilities}"'' else ""
+        ${optionalString (cfg.additionalCapabilities != null && cfg.additionalCapabilities != [])
+          ''--capability="${concatStringsSep "," cfg.additionalCapabilities}"''
         } \
-        ${if cfg.tmpfs != null && cfg.tmpfs != [] then
-          ''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}'' else ""
+        ${optionalString (cfg.tmpfs != null && cfg.tmpfs != [])
+          ''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}''
         } \
         ${containerInit cfg} "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/init"
     '';
@@ -514,6 +514,11 @@ in
                       };
                     in [ extraConfig ] ++ (map (x: x.value) defs);
                   prefix = [ "containers" name ];
+                  inherit (config) specialArgs;
+
+                  # The system is inherited from the host above.
+                  # Set it to null, to remove the "legacy" entrypoint's non-hermetic default.
+                  system = null;
                 }).config;
               };
             };
@@ -555,6 +560,16 @@ in
               '';
             };
 
+            specialArgs = mkOption {
+              type = types.attrsOf types.unspecified;
+              default = {};
+              description = lib.mdDoc ''
+                A set of special arguments to be passed to NixOS modules.
+                This will be merged into the `specialArgs` used to evaluate
+                the NixOS configurations.
+              '';
+            };
+
             ephemeral = mkOption {
               type = types.bool;
               default = false;
@@ -785,14 +800,14 @@ in
       # declarative containers
       ++ (mapAttrsToList (name: cfg: nameValuePair "container@${name}" (let
           containerConfig = cfg // (
-          if cfg.enableTun then
+          optionalAttrs cfg.enableTun
             {
               allowedDevices = cfg.allowedDevices
                 ++ [ { node = "/dev/net/tun"; modifier = "rw"; } ];
               additionalCapabilities = cfg.additionalCapabilities
                 ++ [ "CAP_NET_ADMIN" ];
             }
-          else {});
+          );
         in
           recursiveUpdate unit {
             preStart = preStartScript containerConfig;
@@ -802,7 +817,7 @@ in
             unitConfig.RequiresMountsFor = lib.optional (!containerConfig.ephemeral) "${stateDirectory}/%i";
             environment.root = if containerConfig.ephemeral then "/run/nixos-containers/%i" else "${stateDirectory}/%i";
           } // (
-          if containerConfig.autoStart then
+          optionalAttrs containerConfig.autoStart
             {
               wantedBy = [ "machines.target" ];
               wants = [ "network.target" ];
@@ -813,7 +828,7 @@ in
               ];
               restartIfChanged = true;
             }
-          else {})
+          )
       )) config.containers)
     ));
 
diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix
index 61066c3cbd7..a9f4ab77f86 100644
--- a/nixos/modules/virtualisation/oci-containers.nix
+++ b/nixos/modules/virtualisation/oci-containers.nix
@@ -230,7 +230,10 @@ let
     escapedName = escapeShellArg name;
   in {
     wantedBy = [] ++ optional (container.autoStart) "multi-user.target";
-    after = lib.optionals (cfg.backend == "docker") [ "docker.service" "docker.socket" ] ++ dependsOn;
+    after = lib.optionals (cfg.backend == "docker") [ "docker.service" "docker.socket" ]
+            # if imageFile is not set, the service needs the network to download the image from the registry
+            ++ lib.optionals (container.imageFile == null) [ "network-online.target" ]
+            ++ dependsOn;
     requires = dependsOn;
     environment = proxy_env;
 
diff --git a/nixos/modules/virtualisation/openstack-config.nix b/nixos/modules/virtualisation/openstack-config.nix
index af4f5746610..0ef7a3b5010 100644
--- a/nixos/modules/virtualisation/openstack-config.nix
+++ b/nixos/modules/virtualisation/openstack-config.nix
@@ -59,8 +59,8 @@ in
     # Allow root logins
     services.openssh = {
       enable = true;
-      permitRootLogin = "prohibit-password";
-      passwordAuthentication = mkDefault false;
+      settings.PermitRootLogin = "prohibit-password";
+      settings.PasswordAuthentication = mkDefault false;
     };
 
     users.users.root.initialPassword = "foobar";
diff --git a/nixos/modules/virtualisation/openstack-options.nix b/nixos/modules/virtualisation/openstack-options.nix
index c71b581b02c..52f45de92ec 100644
--- a/nixos/modules/virtualisation/openstack-options.nix
+++ b/nixos/modules/virtualisation/openstack-options.nix
@@ -29,13 +29,13 @@ in
             options = {
               mount = lib.mkOption {
                 description = lib.mdDoc "Where to mount this dataset.";
-                type = types.nullOr types.string;
+                type = types.nullOr types.str;
                 default = null;
               };
 
               properties = lib.mkOption {
                 description = lib.mdDoc "Properties to set on this dataset.";
-                type = types.attrsOf types.string;
+                type = types.attrsOf types.str;
                 default = { };
               };
             };
diff --git a/nixos/modules/virtualisation/parallels-guest.nix b/nixos/modules/virtualisation/parallels-guest.nix
index 07a61bf208d..dba8ce02b72 100644
--- a/nixos/modules/virtualisation/parallels-guest.nix
+++ b/nixos/modules/virtualisation/parallels-guest.nix
@@ -87,7 +87,6 @@ in
       bindsTo = [ "cups.service" ];
       path = [ prl-tools ];
       serviceConfig = {
-        Type = "forking";
         ExecStart = "${prl-tools}/bin/prlshprint";
         WorkingDirectory = "${prl-tools}/bin";
       };
diff --git a/nixos/modules/virtualisation/podman/default.nix b/nixos/modules/virtualisation/podman/default.nix
index 13bbb4471ea..c3fae4bac41 100644
--- a/nixos/modules/virtualisation/podman/default.nix
+++ b/nixos/modules/virtualisation/podman/default.nix
@@ -1,13 +1,14 @@
 { config, lib, pkgs, ... }:
 let
   cfg = config.virtualisation.podman;
-  toml = pkgs.formats.toml { };
   json = pkgs.formats.json { };
 
   inherit (lib) mkOption types;
 
   podmanPackage = (pkgs.podman.override {
     extraPackages = cfg.extraPackages
+      # setuid shadow
+      ++ [ "/run/wrappers" ]
       ++ lib.optional (builtins.elem "zfs" config.boot.supportedFilesystems) config.boot.zfs.package;
   });
 
@@ -27,24 +28,13 @@ let
     done
   '';
 
-  net-conflist = pkgs.runCommand "87-podman-bridge.conflist"
-    {
-      nativeBuildInputs = [ pkgs.jq ];
-      extraPlugins = builtins.toJSON cfg.defaultNetwork.extraPlugins;
-      jqScript = ''
-        . + { "plugins": (.plugins + $extraPlugins) }
-      '';
-    } ''
-    jq <${cfg.package}/etc/cni/net.d/87-podman-bridge.conflist \
-      --argjson extraPlugins "$extraPlugins" \
-      "$jqScript" \
-      >$out
-  '';
-
 in
 {
   imports = [
-    ./dnsname.nix
+    (lib.mkRemovedOptionModule [ "virtualisation" "podman" "defaultNetwork" "dnsname" ]
+      "Use virtualisation.podman.defaultNetwork.settings.dns_enabled instead.")
+    (lib.mkRemovedOptionModule [ "virtualisation" "podman" "defaultNetwork" "extraPlugins" ]
+      "Netavark isn't compatible with CNI plugins.")
     ./network-socket.nix
   ];
 
@@ -149,26 +139,42 @@ in
       '';
     };
 
-    defaultNetwork.extraPlugins = lib.mkOption {
-      type = types.listOf json.type;
-      default = [ ];
+    defaultNetwork.settings = lib.mkOption {
+      type = json.type;
+      default = { };
+      example = lib.literalExpression "{ dns_enabled = true; }";
       description = lib.mdDoc ''
-        Extra CNI plugin configurations to add to podman's default network.
+        Settings for podman's default network.
       '';
     };
 
   };
 
-  config = lib.mkIf cfg.enable (lib.mkMerge [
+  config = lib.mkIf cfg.enable
     {
       environment.systemPackages = [ cfg.package ]
         ++ lib.optional cfg.dockerCompat dockerCompat;
 
-      environment.etc."cni/net.d/87-podman-bridge.conflist".source = net-conflist;
+      # https://github.com/containers/podman/blob/097cc6eb6dd8e598c0e8676d21267b4edb11e144/docs/tutorials/basic_networking.md#default-network
+      environment.etc."containers/networks/podman.json" = lib.mkIf (cfg.defaultNetwork.settings != { }) {
+        source = json.generate "podman.json" ({
+          dns_enabled = false;
+          driver = "bridge";
+          id = "0000000000000000000000000000000000000000000000000000000000000000";
+          internal = false;
+          ipam_options = { driver = "host-local"; };
+          ipv6_enabled = false;
+          name = "podman";
+          network_interface = "podman0";
+          subnets = [{ gateway = "10.88.0.1"; subnet = "10.88.0.0/16"; }];
+        } // cfg.defaultNetwork.settings);
+      };
 
       virtualisation.containers = {
         enable = true; # Enable common /etc/containers configuration
-        containersConf.settings = lib.optionalAttrs cfg.enableNvidia {
+        containersConf.settings = {
+          network.network_backend = "netavark";
+        } // lib.optionalAttrs cfg.enableNvidia {
           engine = {
             conmon_env_vars = [ "PATH=${lib.makeBinPath [ pkgs.nvidia-podman ]}" ];
             runtimes.nvidia = [ "${pkgs.nvidia-podman}/bin/nvidia-container-runtime" ];
@@ -178,10 +184,6 @@ in
 
       systemd.packages = [ cfg.package ];
 
-      systemd.services.podman.serviceConfig = {
-        ExecStart = [ "" "${cfg.package}/bin/podman $LOGGING system service" ];
-      };
-
       systemd.services.podman-prune = {
         description = "Prune podman resources";
 
@@ -202,10 +204,6 @@ in
       systemd.sockets.podman.wantedBy = [ "sockets.target" ];
       systemd.sockets.podman.socketConfig.SocketGroup = "podman";
 
-      systemd.user.services.podman.serviceConfig = {
-        ExecStart = [ "" "${cfg.package}/bin/podman $LOGGING system service" ];
-      };
-
       systemd.user.sockets.podman.wantedBy = [ "sockets.target" ];
 
       systemd.tmpfiles.packages = [
@@ -238,6 +236,5 @@ in
           '';
         }
       ];
-    }
-  ]);
+    };
 }
diff --git a/nixos/modules/virtualisation/podman/dnsname.nix b/nixos/modules/virtualisation/podman/dnsname.nix
deleted file mode 100644
index 3e7d35ae1e4..00000000000
--- a/nixos/modules/virtualisation/podman/dnsname.nix
+++ /dev/null
@@ -1,36 +0,0 @@
-{ config, lib, pkgs, ... }:
-let
-  inherit (lib)
-    mkOption
-    mkIf
-    types
-    ;
-
-  cfg = config.virtualisation.podman;
-
-in
-{
-  options = {
-    virtualisation.podman = {
-
-      defaultNetwork.dnsname.enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Enable DNS resolution in the default podman network.
-        '';
-      };
-
-    };
-  };
-
-  config = {
-    virtualisation.containers.containersConf.cniPlugins = mkIf cfg.defaultNetwork.dnsname.enable [ pkgs.dnsname-cni ];
-    virtualisation.podman.defaultNetwork.extraPlugins =
-      lib.optional cfg.defaultNetwork.dnsname.enable {
-        type = "dnsname";
-        domainName = "dns.podman";
-        capabilities.aliases = true;
-      };
-  };
-}
diff --git a/nixos/modules/virtualisation/proxmox-image.nix b/nixos/modules/virtualisation/proxmox-image.nix
index 6a4220fd265..1074b02d0eb 100644
--- a/nixos/modules/virtualisation/proxmox-image.nix
+++ b/nixos/modules/virtualisation/proxmox-image.nix
@@ -98,10 +98,12 @@ with lib;
     qemuExtraConf = mkOption {
       type = with types; attrsOf (oneOf [ str int ]);
       default = {};
-      example = literalExpression ''{
-        cpu = "host";
-        onboot = 1;
-      }'';
+      example = literalExpression ''
+        {
+          cpu = "host";
+          onboot = 1;
+        }
+      '';
       description = lib.mdDoc ''
         Additional options appended to qemu-server.conf
       '';
@@ -135,10 +137,11 @@ with lib;
     cfgLine = name: value: ''
       ${name}: ${builtins.toString value}
     '';
+    virtio0Storage = builtins.head (builtins.split ":" cfg.qemuConf.virtio0);
     cfgFile = fileName: properties: pkgs.writeTextDir fileName ''
       # generated by NixOS
       ${lib.concatStrings (lib.mapAttrsToList cfgLine properties)}
-      #qmdump#map:virtio0:drive-virtio0:local-lvm:raw:
+      #qmdump#map:virtio0:drive-virtio0:${virtio0Storage}:raw:
     '';
     inherit (cfg) partitionTableType;
     supportEfi = partitionTableType == "efi" || partitionTableType == "hybrid";
@@ -186,23 +189,40 @@ with lib;
           guestAgentSupport = false;
         }).overrideAttrs ( super: rec {
 
-          version = "7.0.0";
+          version = "7.2.1";
           src = pkgs.fetchurl {
             url= "https://download.qemu.org/qemu-${version}.tar.xz";
-            sha256 = "sha256-9rN1x5UfcoQCeYsLqrsthkeMpT1Eztvvq74cRr9G+Dk=";
+            sha256 = "sha256-jIVpms+dekOl/immTN1WNwsMLRrQdLr3CYqCTReq1zs=";
           };
           patches = [
+            # Proxmox' VMA tool is published as a particular patch upon QEMU
             (pkgs.fetchpatch {
               url =
                 let
-                  rev = "1976ca460796f28447b41e3618e5c1e234035dd5";
-                  path = "debian/patches/pve/0026-PVE-Backup-add-vma-backup-format-code.patch";
+                  rev = "abb04bb6272c1202ca9face0827917552b9d06f6";
+                  path = "debian/patches/pve/0027-PVE-Backup-add-vma-backup-format-code.patch";
                 in "https://git.proxmox.com/?p=pve-qemu.git;a=blob_plain;hb=${rev};f=${path}";
-              hash = "sha256-2Dz+ceTwrcyYYxi76RtyY3v15/2pwGcDhFuoZWlgbjc=";
+              hash = "sha256-3d0HHdvaExCry6zcULnziYnWIAnn24vECkI4sjj2BMg=";
             })
+
+            # Proxmox' VMA tool uses O_DIRECT which fails on tmpfs
+            # Filed to upstream issue tracker: https://bugzilla.proxmox.com/show_bug.cgi?id=4710
+            (pkgs.writeText "inline.patch" ''
+                --- a/vma-writer.c   2023-05-01 15:11:13.361341177 +0200
+                +++ b/vma-writer.c   2023-05-01 15:10:51.785293129 +0200
+                @@ -306,7 +306,7 @@
+                             /* try to use O_NONBLOCK */
+                             fcntl(vmaw->fd, F_SETFL, fcntl(vmaw->fd, F_GETFL)|O_NONBLOCK);
+                         } else  {
+                -            oflags = O_NONBLOCK|O_DIRECT|O_WRONLY|O_EXCL;
+                +            oflags = O_NONBLOCK|O_WRONLY|O_EXCL;
+                             vmaw->fd = qemu_create(filename, oflags, 0644, errp);
+                         }
+            '')
           ];
 
           buildInputs = super.buildInputs ++ [ pkgs.libuuid ];
+          nativeBuildInputs = super.nativeBuildInputs ++ [ pkgs.perl ];
 
         });
       in
diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix
index 1b3c0e23f97..d0a5ddd87cc 100644
--- a/nixos/modules/virtualisation/qemu-vm.nix
+++ b/nixos/modules/virtualisation/qemu-vm.nix
@@ -1,11 +1,8 @@
 # This module creates a virtual machine from the NixOS configuration.
 # Building the `config.system.build.vm' attribute gives you a command
 # that starts a KVM/QEMU VM running the NixOS configuration defined in
-# `config'.  The Nix store is shared read-only with the host, which
-# makes (re)building VMs very efficient.  However, it also means you
-# can't reconfigure the guest inside the guest - you need to rebuild
-# the VM in the host.  On the other hand, the root filesystem is a
-# read/writable disk image persistent across VM reboots.
+# `config'. By default, the Nix store is shared read-only with the
+# host, which makes (re)building VMs very efficient.
 
 { config, lib, pkgs, options, ... }:
 
@@ -21,6 +18,8 @@ let
 
   qemu = cfg.qemu.package;
 
+  hostPkgs = cfg.host.pkgs;
+
   consoles = lib.concatMapStringsSep " " (c: "console=${c}") cfg.qemu.consoles;
 
   driveOpts = { ... }: {
@@ -55,6 +54,11 @@ let
 
   };
 
+  selectPartitionTableLayout = { useEFIBoot, useDefaultFilesystems }:
+  if useDefaultFilesystems then
+    if useEFIBoot then "efi" else "legacy"
+  else "none";
+
   driveCmdline = idx: { file, driveExtraOpts, deviceExtraOpts, ... }:
     let
       drvId = "drive${toString idx}";
@@ -79,40 +83,51 @@ let
 
   drivesCmdLine = drives: concatStringsSep "\\\n    " (imap1 driveCmdline drives);
 
-
-  # Creates a device name from a 1-based a numerical index, e.g.
-  # * `driveDeviceName 1` -> `/dev/vda`
-  # * `driveDeviceName 2` -> `/dev/vdb`
-  driveDeviceName = idx:
-    let letter = elemAt lowerChars (idx - 1);
-    in if cfg.qemu.diskInterface == "scsi" then
-      "/dev/sd${letter}"
-    else
-      "/dev/vd${letter}";
-
-  lookupDriveDeviceName = driveName: driveList:
-    (findSingle (drive: drive.name == driveName)
-      (throw "Drive ${driveName} not found")
-      (throw "Multiple drives named ${driveName}") driveList).device;
-
-  addDeviceNames =
-    imap1 (idx: drive: drive // { device = driveDeviceName idx; });
-
-
   # Shell script to start the VM.
   startVM =
     ''
-      #! ${cfg.host.pkgs.runtimeShell}
+      #! ${hostPkgs.runtimeShell}
 
-      export PATH=${makeBinPath [ cfg.host.pkgs.coreutils ]}''${PATH:+:}$PATH
+      export PATH=${makeBinPath [ hostPkgs.coreutils ]}''${PATH:+:}$PATH
 
       set -e
 
-      NIX_DISK_IMAGE=$(readlink -f "''${NIX_DISK_IMAGE:-${config.virtualisation.diskImage}}")
+      # Create an empty ext4 filesystem image. A filesystem image does not
+      # contain a partition table but just a filesystem.
+      createEmptyFilesystemImage() {
+        local name=$1
+        local size=$2
+        local temp=$(mktemp)
+        ${qemu}/bin/qemu-img create -f raw "$temp" "$size"
+        ${hostPkgs.e2fsprogs}/bin/mkfs.ext4 -L ${rootFilesystemLabel} "$temp"
+        ${qemu}/bin/qemu-img convert -f raw -O qcow2 "$temp" "$name"
+        rm "$temp"
+      }
 
-      if ! test -e "$NIX_DISK_IMAGE"; then
-          ${qemu}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" \
-            ${toString config.virtualisation.diskSize}M
+      NIX_DISK_IMAGE=$(readlink -f "''${NIX_DISK_IMAGE:-${toString config.virtualisation.diskImage}}") || test -z "$NIX_DISK_IMAGE"
+
+      if test -n "$NIX_DISK_IMAGE" && ! test -e "$NIX_DISK_IMAGE"; then
+          echo "Disk image do not exist, creating the virtualisation disk image..."
+
+          ${if (cfg.useBootLoader && cfg.useDefaultFilesystems) then ''
+            # Create a writable qcow2 image using the systemImage as a backing
+            # image.
+
+            # CoW prevent size to be attributed to an image.
+            # FIXME: raise this issue to upstream.
+            ${qemu}/bin/qemu-img create \
+              -f qcow2 \
+              -b ${systemImage}/nixos.qcow2 \
+              -F qcow2 \
+              "$NIX_DISK_IMAGE"
+          '' else if cfg.useDefaultFilesystems then ''
+            createEmptyFilesystemImage "$NIX_DISK_IMAGE" "${toString cfg.diskSize}M"
+          '' else ''
+            # Create an empty disk image without a filesystem.
+            ${qemu}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" "${toString cfg.diskSize}M"
+          ''
+          }
+          echo "Virtualisation disk image created."
       fi
 
       # Create a directory for storing temporary data of the running VM.
@@ -129,16 +144,17 @@ let
           else ''
             (
               cd ${builtins.storeDir}
-              ${pkgs.erofs-utils}/bin/mkfs.erofs \
+              ${hostPkgs.erofs-utils}/bin/mkfs.erofs \
                 --force-uid=0 \
                 --force-gid=0 \
+                -L ${nixStoreFilesystemLabel} \
                 -U eb176051-bd15-49b7-9e6b-462e0b467019 \
                 -T 0 \
                 --exclude-regex="$(
-                  <${pkgs.closureInfo { rootPaths = [ config.system.build.toplevel regInfo ]; }}/store-paths \
+                  <${hostPkgs.closureInfo { rootPaths = [ config.system.build.toplevel regInfo ]; }}/store-paths \
                     sed -e 's^.*/^^g' \
                   | cut -c -10 \
-                  | ${pkgs.python3}/bin/python ${./includes-to-excludes.py} )" \
+                  | ${hostPkgs.python3}/bin/python ${./includes-to-excludes.py} )" \
                 "$TMPDIR"/store.img \
                 . \
                 </dev/null >/dev/null
@@ -150,22 +166,36 @@ let
       # Create a directory for exchanging data with the VM.
       mkdir -p "$TMPDIR/xchg"
 
-      ${lib.optionalString cfg.useBootLoader
+      ${lib.optionalString cfg.useHostCerts
       ''
-        # Create a writable copy/snapshot of the boot disk.
-        # A writable boot disk can be booted from automatically.
-        ${qemu}/bin/qemu-img create -f qcow2 -F qcow2 -b ${bootDisk}/disk.img "$TMPDIR/disk.img"
-
-        NIX_EFI_VARS=$(readlink -f "''${NIX_EFI_VARS:-${cfg.efiVars}}")
+        mkdir -p "$TMPDIR/certs"
+        if [ -e "$NIX_SSL_CERT_FILE" ]; then
+          cp -L "$NIX_SSL_CERT_FILE" "$TMPDIR"/certs/ca-certificates.crt
+        else
+          echo \$NIX_SSL_CERT_FILE should point to a valid file if virtualisation.useHostCerts is enabled.
+        fi
+      ''}
 
-        ${lib.optionalString cfg.useEFIBoot
-        ''
-          # VM needs writable EFI vars
-          if ! test -e "$NIX_EFI_VARS"; then
-            cp ${bootDisk}/efi-vars.fd "$NIX_EFI_VARS"
-            chmod 0644 "$NIX_EFI_VARS"
-          fi
-        ''}
+      ${lib.optionalString cfg.useEFIBoot
+      ''
+        # Expose EFI variables, it's useful even when we are not using a bootloader (!).
+        # We might be interested in having EFI variable storage present even if we aren't booting via UEFI, hence
+        # no guard against `useBootLoader`.  Examples:
+        # - testing PXE boot or other EFI applications
+        # - directbooting LinuxBoot, which `kexec()s` into a UEFI environment that can boot e.g. Windows
+        NIX_EFI_VARS=$(readlink -f "''${NIX_EFI_VARS:-${config.system.name}-efi-vars.fd}")
+        # VM needs writable EFI vars
+        if ! test -e "$NIX_EFI_VARS"; then
+        ${if cfg.useBootLoader then
+            # We still need the EFI var from the make-disk-image derivation
+            # because our "switch-to-configuration" process might
+            # write into it and we want to keep this data.
+            ''cp ${systemImage}/efi-vars.fd "$NIX_EFI_VARS"''
+            else
+            ''cp ${cfg.efi.variables} "$NIX_EFI_VARS"''
+          }
+          chmod 0644 "$NIX_EFI_VARS"
+        fi
       ''}
 
       cd "$TMPDIR"
@@ -196,105 +226,55 @@ let
     '';
 
 
-  regInfo = pkgs.closureInfo { rootPaths = config.virtualisation.additionalPaths; };
+  regInfo = hostPkgs.closureInfo { rootPaths = config.virtualisation.additionalPaths; };
 
+  # Use well-defined and persistent filesystem labels to identify block devices.
+  rootFilesystemLabel = "nixos";
+  espFilesystemLabel = "ESP"; # Hard-coded by make-disk-image.nix
+  nixStoreFilesystemLabel = "nix-store";
 
-  # Generate a hard disk image containing a /boot partition and GRUB
-  # in the MBR.  Used when the `useBootLoader' option is set.
-  # Uses `runInLinuxVM` to create the image in a throwaway VM.
-  # See note [Disk layout with `useBootLoader`].
-  # FIXME: use nixos/lib/make-disk-image.nix.
-  bootDisk =
-    pkgs.vmTools.runInLinuxVM (
-      pkgs.runCommand "nixos-boot-disk"
-        { preVM =
-            ''
-              mkdir $out
-              diskImage=$out/disk.img
-              ${qemu}/bin/qemu-img create -f qcow2 $diskImage "60M"
-              ${if cfg.useEFIBoot then ''
-                efiVars=$out/efi-vars.fd
-                cp ${cfg.efi.variables} $efiVars
-                chmod 0644 $efiVars
-              '' else ""}
-            '';
-          buildInputs = [ pkgs.util-linux ];
-          QEMU_OPTS = "-nographic -serial stdio -monitor none"
-                      + lib.optionalString cfg.useEFIBoot (
-                        " -drive if=pflash,format=raw,unit=0,readonly=on,file=${cfg.efi.firmware}"
-                      + " -drive if=pflash,format=raw,unit=1,file=$efiVars");
-        }
-        ''
-          # Create a /boot EFI partition with 60M and arbitrary but fixed GUIDs for reproducibility
-          ${pkgs.gptfdisk}/bin/sgdisk \
-            --set-alignment=1 --new=1:34:2047 --change-name=1:BIOSBootPartition --typecode=1:ef02 \
-            --set-alignment=512 --largest-new=2 --change-name=2:EFISystem --typecode=2:ef00 \
-            --attributes=1:set:1 \
-            --attributes=2:set:2 \
-            --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C1 \
-            --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
-            --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
-            --hybrid 2 \
-            --recompute-chs /dev/vda
-
-          ${optionalString (config.boot.loader.grub.device != "/dev/vda")
-            # In this throwaway VM, we only have the /dev/vda disk, but the
-            # actual VM described by `config` (used by `switch-to-configuration`
-            # below) may set `boot.loader.grub.device` to a different device
-            # that's nonexistent in the throwaway VM.
-            # Create a symlink for that device, so that the `grub-install`
-            # by `switch-to-configuration` will hit /dev/vda anyway.
-            ''
-              ln -s /dev/vda ${config.boot.loader.grub.device}
-            ''
-          }
+  # The root drive is a raw disk which does not necessarily contain a
+  # filesystem or partition table. It thus cannot be identified via the typical
+  # persistent naming schemes (e.g. /dev/disk/by-{label, uuid, partlabel,
+  # partuuid}. Instead, supply a well-defined and persistent serial attribute
+  # via QEMU. Inside the running system, the disk can then be identified via
+  # the /dev/disk/by-id scheme.
+  rootDriveSerialAttr = "root";
 
-          ${pkgs.dosfstools}/bin/mkfs.fat -F16 /dev/vda2
-          export MTOOLS_SKIP_CHECK=1
-          ${pkgs.mtools}/bin/mlabel -i /dev/vda2 ::boot
-
-          # Mount /boot; load necessary modules first.
-          ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_cp437.ko.xz || true
-          ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_iso8859-1.ko.xz || true
-          ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/fat.ko.xz || true
-          ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/vfat.ko.xz || true
-          ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/efivarfs/efivarfs.ko.xz || true
-          mkdir /boot
-          mount /dev/vda2 /boot
-
-          ${optionalString config.boot.loader.efi.canTouchEfiVariables ''
-            mount -t efivarfs efivarfs /sys/firmware/efi/efivars
-          ''}
-
-          # This is needed for GRUB 0.97, which doesn't know about virtio devices.
-          mkdir /boot/grub
-          echo '(hd0) /dev/vda' > /boot/grub/device.map
-
-          # This is needed for systemd-boot to find ESP, and udev is not available here to create this
-          mkdir -p /dev/block
-          ln -s /dev/vda2 /dev/block/254:2
-
-          # Set up system profile (normally done by nixos-rebuild / nix-env --set)
-          mkdir -p /nix/var/nix/profiles
-          ln -s ${config.system.build.toplevel} /nix/var/nix/profiles/system-1-link
-          ln -s /nix/var/nix/profiles/system-1-link /nix/var/nix/profiles/system
-
-          # Install bootloader
-          touch /etc/NIXOS
-          export NIXOS_INSTALL_BOOTLOADER=1
-          ${config.system.build.toplevel}/bin/switch-to-configuration boot
-
-          umount /boot
-        '' # */
-    );
+  # System image is akin to a complete NixOS install with
+  # a boot partition and root partition.
+  systemImage = import ../../lib/make-disk-image.nix {
+    inherit pkgs config lib;
+    additionalPaths = [ regInfo ];
+    format = "qcow2";
+    onlyNixStore = false;
+    label = rootFilesystemLabel;
+    partitionTableType = selectPartitionTableLayout { inherit (cfg) useDefaultFilesystems useEFIBoot; };
+    # Bootloader should be installed on the system image only if we are booting through bootloaders.
+    # Though, if a user is not using our default filesystems, it is possible to not have any ESP
+    # or a strange partition table that's incompatible with GRUB configuration.
+    # As a consequence, this may lead to disk image creation failures.
+    # To avoid this, we prefer to let the user find out about how to install the bootloader on its ESP/disk.
+    # Usually, this can be through building your own disk image.
+    # TODO: If a user is interested into a more fine grained heuristic for `installBootLoader`
+    # by examining the actual contents of `cfg.fileSystems`, please send a PR.
+    installBootLoader = cfg.useBootLoader && cfg.useDefaultFilesystems;
+    touchEFIVars = cfg.useEFIBoot;
+    diskSize = "auto";
+    additionalSpace = "0M";
+    copyChannel = false;
+    OVMF = cfg.efi.OVMF;
+  };
 
   storeImage = import ../../lib/make-disk-image.nix {
     inherit pkgs config lib;
     additionalPaths = [ regInfo ];
     format = "qcow2";
     onlyNixStore = true;
+    label = nixStoreFilesystemLabel;
     partitionTableType = "none";
     installBootLoader = false;
+    touchEFIVars = false;
     diskSize = "auto";
     additionalSpace = "0M";
     copyChannel = false;
@@ -306,6 +286,9 @@ in
   imports = [
     ../profiles/qemu-guest.nix
     (mkRenamedOptionModule [ "virtualisation" "pathsInNixDB" ] [ "virtualisation" "additionalPaths" ])
+    (mkRemovedOptionModule [ "virtualisation" "bootDevice" ] "This option was renamed to `virtualisation.rootDevice`, as it was incorrectly named and misleading. Take the time to review what you want to do and look at the new options like `virtualisation.{bootLoaderDevice, bootPartition}`, open an issue in case of issues.")
+    (mkRemovedOptionModule [ "virtualisation" "efiVars" ] "This option was removed, it is possible to provide a template UEFI variable with `virtualisation.efi.variables` ; if this option is important to you, open an issue")
+    (mkRemovedOptionModule [ "virtualisation" "persistBootDevice" ] "Boot device is always persisted if you use a bootloader through the root disk image ; if this does not work for your usecase, please examine carefully what `virtualisation.{bootDevice, rootDevice, bootPartition}` options offer you and open an issue explaining your need.`")
   ];
 
   options = {
@@ -346,7 +329,7 @@ in
 
     virtualisation.diskImage =
       mkOption {
-        type = types.str;
+        type = types.nullOr types.str;
         default = "./${config.system.name}.qcow2";
         defaultText = literalExpression ''"./''${config.system.name}.qcow2"'';
         description =
@@ -354,16 +337,48 @@ in
             Path to the disk image containing the root filesystem.
             The image will be created on startup if it does not
             exist.
+
+            If null, a tmpfs will be used as the root filesystem and
+            the VM's state will not be persistent.
           '';
       };
 
-    virtualisation.bootDevice =
+    virtualisation.bootLoaderDevice =
       mkOption {
         type = types.path;
-        example = "/dev/vda";
+        default = "/dev/disk/by-id/virtio-${rootDriveSerialAttr}";
+        defaultText = literalExpression ''/dev/disk/by-id/virtio-${rootDriveSerialAttr}'';
+        example = "/dev/disk/by-id/virtio-boot-loader-device";
         description =
           lib.mdDoc ''
-            The disk to be used for the root filesystem.
+            The path (inside th VM) to the device to boot from when legacy booting.
+          '';
+        };
+
+    virtualisation.bootPartition =
+      mkOption {
+        type = types.nullOr types.path;
+        default = if cfg.useEFIBoot then "/dev/disk/by-label/${espFilesystemLabel}" else null;
+        defaultText = literalExpression ''if cfg.useEFIBoot then "/dev/disk/by-label/${espFilesystemLabel}" else null'';
+        example = "/dev/disk/by-label/esp";
+        description =
+          lib.mdDoc ''
+            The path (inside the VM) to the device containing the EFI System Partition (ESP).
+
+            If you are *not* booting from a UEFI firmware, this value is, by
+            default, `null`. The ESP is mounted under `/boot`.
+          '';
+      };
+
+    virtualisation.rootDevice =
+      mkOption {
+        type = types.nullOr types.path;
+        default = "/dev/disk/by-label/${rootFilesystemLabel}";
+        defaultText = literalExpression ''/dev/disk/by-label/${rootFilesystemLabel}'';
+        example = "/dev/disk/by-label/nixos";
+        description =
+          lib.mdDoc ''
+            The path (inside the VM) to the device containing the root filesystem.
           '';
       };
 
@@ -528,10 +543,25 @@ in
         '';
     };
 
+    virtualisation.restrictNetwork =
+      mkOption {
+        type = types.bool;
+        default = false;
+        example = true;
+        description =
+          lib.mdDoc ''
+            If this option is enabled, the guest will be isolated, i.e. it will
+            not be able to contact the host and no guest IP packets will be
+            routed over the host to the outside. This option does not affect
+            any explicitly set forwarding rules.
+          '';
+      };
+
     virtualisation.vlans =
       mkOption {
         type = types.listOf types.ints.unsigned;
-        default = [ 1 ];
+        default = if config.virtualisation.interfaces == {} then [ 1 ] else [ ];
+        defaultText = lib.literalExpression ''if config.virtualisation.interfaces == {} then [ 1 ] else [ ]'';
         example = [ 1 2 ];
         description =
           lib.mdDoc ''
@@ -546,15 +576,47 @@ in
           '';
       };
 
+    virtualisation.interfaces = mkOption {
+      default = {};
+      example = {
+        enp1s0.vlan = 1;
+      };
+      description = lib.mdDoc ''
+        Network interfaces to add to the VM.
+      '';
+      type = with types; attrsOf (submodule {
+        options = {
+          vlan = mkOption {
+            type = types.ints.unsigned;
+            description = lib.mdDoc ''
+              VLAN to which the network interface is connected.
+            '';
+          };
+
+          assignIP = mkOption {
+            type = types.bool;
+            default = false;
+            description = lib.mdDoc ''
+              Automatically assign an IP address to the network interface using the same scheme as
+              virtualisation.vlans.
+            '';
+          };
+        };
+      });
+    };
+
     virtualisation.writableStore =
       mkOption {
         type = types.bool;
-        default = true; # FIXME
+        default = cfg.mountHostNixStore;
+        defaultText = literalExpression "cfg.mountHostNixStore";
         description =
           lib.mdDoc ''
             If enabled, the Nix store in the VM is made writable by
             layering an overlay filesystem on top of the host's Nix
             store.
+
+            By default, this is enabled if you mount a host Nix store.
           '';
       };
 
@@ -594,7 +656,7 @@ in
       package =
         mkOption {
           type = types.package;
-          default = cfg.host.pkgs.qemu_kvm;
+          default = hostPkgs.qemu_kvm;
           defaultText = literalExpression "config.virtualisation.host.pkgs.qemu_kvm";
           example = literalExpression "pkgs.qemu_test";
           description = lib.mdDoc "QEMU package to use.";
@@ -647,7 +709,6 @@ in
         mkOption {
           type = types.listOf (types.submodule driveOpts);
           description = lib.mdDoc "Drives passed to qemu.";
-          apply = addDeviceNames;
         };
 
       diskInterface =
@@ -688,21 +749,69 @@ in
           For applications which do a lot of reads from the store,
           this can drastically improve performance, but at the cost of
           disk space and image build time.
+
+          As an alternative, you can use a bootloader which will provide you
+          with a full NixOS system image containing a Nix store and
+          avoid mounting the host nix store through
+          {option}`virtualisation.mountHostNixStore`.
+        '';
+      };
+
+    virtualisation.mountHostNixStore =
+      mkOption {
+        type = types.bool;
+        default = !cfg.useNixStoreImage && !cfg.useBootLoader;
+        defaultText = literalExpression "!cfg.useNixStoreImage && !cfg.useBootLoader";
+        description = lib.mdDoc ''
+          Mount the host Nix store as a 9p mount.
         '';
       };
 
+    virtualisation.directBoot = {
+      enable =
+        mkOption {
+          type = types.bool;
+          default = !cfg.useBootLoader;
+          defaultText = "!cfg.useBootLoader";
+          description =
+            lib.mdDoc ''
+              If enabled, the virtual machine will boot directly into the kernel instead of through a bootloader. Other relevant parameters such as the initrd are also passed to QEMU.
+
+              If you want to test netboot, consider disabling this option.
+
+              This will not boot / reboot correctly into a system that has switched to a different configuration on disk.
+
+              This is enabled by default if you don't enable bootloaders, but you can still enable a bootloader if you need.
+              Read more about this feature: <https://qemu-project.gitlab.io/qemu/system/linuxboot.html>.
+            '';
+        };
+      initrd =
+        mkOption {
+          type = types.str;
+          default = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
+          defaultText = "\${config.system.build.initialRamdisk}/\${config.system.boot.loader.initrdFile}";
+          description =
+            lib.mdDoc ''
+              In direct boot situations, you may want to influence the initrd to load
+              to use your own customized payload.
+
+              This is useful if you want to test the netboot image without
+              testing the firmware or the loading part.
+            '';
+        };
+    };
+
     virtualisation.useBootLoader =
       mkOption {
         type = types.bool;
         default = false;
         description =
           lib.mdDoc ''
-            If enabled, the virtual machine will be booted using the
-            regular boot loader (i.e., GRUB 1 or 2).  This allows
-            testing of the boot loader.  If
-            disabled (the default), the VM directly boots the NixOS
-            kernel and initial ramdisk, bypassing the boot loader
-            altogether.
+            Use a boot loader to boot the system.
+            This allows, among other things, testing the boot loader.
+
+            If disabled, the kernel and initrd are directly booted,
+            forgoing any bootloader.
           '';
       };
 
@@ -719,10 +828,22 @@ in
         };
 
     virtualisation.efi = {
+      OVMF = mkOption {
+        type = types.package;
+        default = (pkgs.OVMF.override {
+          secureBoot = cfg.useSecureBoot;
+        }).fd;
+        defaultText = ''(pkgs.OVMF.override {
+          secureBoot = cfg.useSecureBoot;
+        }).fd'';
+        description =
+        lib.mdDoc "OVMF firmware package, defaults to OVMF configured with secure boot if needed.";
+      };
+
       firmware = mkOption {
         type = types.path;
-        default = pkgs.OVMF.firmware;
-        defaultText = literalExpression "pkgs.OVMF.firmware";
+        default = cfg.efi.OVMF.firmware;
+        defaultText = literalExpression "cfg.efi.OVMF.firmware";
         description =
           lib.mdDoc ''
             Firmware binary for EFI implementation, defaults to OVMF.
@@ -731,8 +852,8 @@ in
 
       variables = mkOption {
         type = types.path;
-        default = pkgs.OVMF.variables;
-        defaultText = literalExpression "pkgs.OVMF.variables";
+        default = cfg.efi.OVMF.variables;
+        defaultText = literalExpression "cfg.efi.OVMF.variables";
         description =
           lib.mdDoc ''
             Platform-specific flash binary for EFI variables, implementation-dependent to the EFI firmware.
@@ -756,15 +877,13 @@ in
           '';
       };
 
-    virtualisation.efiVars =
+    virtualisation.useSecureBoot =
       mkOption {
-        type = types.str;
-        default = "./${config.system.name}-efi-vars.fd";
-        defaultText = literalExpression ''"./''${config.system.name}-efi-vars.fd"'';
+        type = types.bool;
+        default = false;
         description =
           lib.mdDoc ''
-            Path to nvram image containing UEFI variables.  The will be created
-            on startup if it does not exist.
+            Enable Secure Boot support in the EFI firmware.
           '';
       };
 
@@ -780,6 +899,17 @@ in
           '';
       };
 
+    virtualisation.useHostCerts =
+      mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          lib.mdDoc ''
+            If enabled, when `NIX_SSL_CERT_FILE` is set on the host,
+            pass the CA certificates from the host to the VM.
+          '';
+      };
+
   };
 
   config = {
@@ -801,7 +931,25 @@ in
                   The address must be in the default VLAN (10.0.2.0/24).
               '';
           }
-        ]));
+        ])) ++ [
+          { assertion = pkgs.stdenv.hostPlatform.is32bit -> cfg.memorySize < 2047;
+            message = ''
+              virtualisation.memorySize is above 2047, but qemu is only able to allocate 2047MB RAM on 32bit max.
+            '';
+          }
+          { assertion = cfg.directBoot.initrd != options.virtualisation.directBoot.initrd.default -> cfg.directBoot.enable;
+            message =
+              ''
+                You changed the default of `virtualisation.directBoot.initrd` but you are not
+                using QEMU direct boot. This initrd will not be used in your current
+                boot configuration.
+
+                Either do not mutate `virtualisation.directBoot.initrd` or enable direct boot.
+
+                If you have a more advanced usecase, please open an issue or a pull request.
+              '';
+          }
+        ];
 
     warnings =
       optional (
@@ -821,49 +969,23 @@ in
           Otherwise, we recommend
 
             ${opt.writableStore} = false;
+            ''
+      ++ optional (cfg.directBoot.enable && cfg.useBootLoader)
+        ''
+          You enabled direct boot and a bootloader, QEMU will not boot your bootloader, rendering
+          `useBootLoader` useless. You might want to disable one of those options.
         '';
 
-    # Note [Disk layout with `useBootLoader`]
-    #
-    # If `useBootLoader = true`, we configure 2 drives:
-    # `/dev/?da` for the root disk, and `/dev/?db` for the boot disk
-    # which has the `/boot` partition and the boot loader.
-    # Concretely:
-    #
-    # * The second drive's image `disk.img` is created in `bootDisk = ...`
-    #   using a throwaway VM. Note that there the disk is always `/dev/vda`,
-    #   even though in the final VM it will be at `/dev/*b`.
-    # * The disks are attached in `virtualisation.qemu.drives`.
-    #   Their order makes them appear as devices `a`, `b`, etc.
-    # * `fileSystems."/boot"` is adjusted to be on device `b`.
-
-    # If `useBootLoader`, GRUB goes to the second disk, see
-    # note [Disk layout with `useBootLoader`].
-    boot.loader.grub.device = mkVMOverride (
-      if cfg.useBootLoader
-        then driveDeviceName 2 # second disk
-        else cfg.bootDevice
-    );
+    # In UEFI boot, we use a EFI-only partition table layout, thus GRUB will fail when trying to install
+    # legacy and UEFI. In order to avoid this, we have to put "nodev" to force UEFI-only installs.
+    # Otherwise, we set the proper bootloader device for this.
+    # FIXME: make a sense of this mess wrt to multiple ESP present in the system, probably use boot.efiSysMountpoint?
+    boot.loader.grub.device = mkVMOverride (if cfg.useEFIBoot then "nodev" else cfg.bootLoaderDevice);
     boot.loader.grub.gfxmodeBios = with cfg.resolution; "${toString x}x${toString y}";
 
     boot.initrd.kernelModules = optionals (cfg.useNixStoreImage && !cfg.writableStore) [ "erofs" ];
 
-    boot.initrd.extraUtilsCommands = lib.mkIf (cfg.useDefaultFilesystems && !config.boot.initrd.systemd.enable)
-      ''
-        # We need mke2fs in the initrd.
-        copy_bin_and_libs ${pkgs.e2fsprogs}/bin/mke2fs
-      '';
-
-    boot.initrd.postDeviceCommands = lib.mkIf (cfg.useDefaultFilesystems && !config.boot.initrd.systemd.enable)
-      ''
-        # If the disk image appears to be empty, run mke2fs to
-        # initialise.
-        FSTYPE=$(blkid -o value -s TYPE ${cfg.bootDevice} || true)
-        PARTTYPE=$(blkid -o value -s PTTYPE ${cfg.bootDevice} || true)
-        if test -z "$FSTYPE" -a -z "$PARTTYPE"; then
-            mke2fs -t ext4 ${cfg.bootDevice}
-        fi
-      '';
+    boot.loader.supportsInitrdSecrets = mkIf (!cfg.useBootLoader) (mkVMOverride false);
 
     boot.initrd.postMountCommands = lib.mkIf (!config.boot.initrd.systemd.enable)
       ''
@@ -878,7 +1000,7 @@ in
 
         ${optionalString cfg.writableStore ''
           echo "mounting overlay filesystem on /nix/store..."
-          mkdir -p 0755 $targetRoot/nix/.rw-store/store $targetRoot/nix/.rw-store/work $targetRoot/nix/store
+          mkdir -p -m 0755 $targetRoot/nix/.rw-store/store $targetRoot/nix/.rw-store/work $targetRoot/nix/store
           mount -t overlay overlay $targetRoot/nix/store \
             -o lowerdir=$targetRoot/nix/.ro-store,upperdir=$targetRoot/nix/.rw-store/store,workdir=$targetRoot/nix/.rw-store/work || fail
         ''}
@@ -907,12 +1029,10 @@ in
       optional cfg.writableStore "overlay"
       ++ optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx";
 
-    virtualisation.bootDevice = mkDefault (driveDeviceName 1);
-
     virtualisation.additionalPaths = [ config.system.build.toplevel ];
 
     virtualisation.sharedDirectories = {
-      nix-store = mkIf (!cfg.useNixStoreImage) {
+      nix-store = mkIf cfg.mountHostNixStore {
         source = builtins.storeDir;
         target = "/nix/store";
       };
@@ -924,8 +1044,14 @@ in
         source = ''"''${SHARED_DIR:-$TMPDIR/xchg}"'';
         target = "/tmp/shared";
       };
+      certs = mkIf cfg.useHostCerts {
+        source = ''"$TMPDIR"/certs'';
+        target = "/etc/ssl/certs";
+      };
     };
 
+    security.pki.installCACerts = mkIf cfg.useHostCerts false;
+
     virtualisation.qemu.networkingOptions =
       let
         forwardingOptions = flip concatMapStrings cfg.forwardPorts
@@ -936,13 +1062,13 @@ in
               else "'guestfwd=${proto}:${guest.address}:${toString guest.port}-" +
                    "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}',"
           );
+        restrictNetworkOption = lib.optionalString cfg.restrictNetwork "restrict=on,";
       in
       [
         "-net nic,netdev=user.0,model=virtio"
-        "-netdev user,id=user.0,${forwardingOptions}\"$QEMU_NET_OPTS\""
+        "-netdev user,id=user.0,${forwardingOptions}${restrictNetworkOption}\"$QEMU_NET_OPTS\""
       ];
 
-    # FIXME: Consolidate this one day.
     virtualisation.qemu.options = mkMerge [
       (mkIf cfg.qemu.virtioKeyboard [
         "-device virtio-keyboard"
@@ -957,14 +1083,14 @@ in
         alphaNumericChars = lowerChars ++ upperChars ++ (map toString (range 0 9));
         # Replace all non-alphanumeric characters with underscores
         sanitizeShellIdent = s: concatMapStrings (c: if builtins.elem c alphaNumericChars then c else "_") (stringToCharacters s);
-      in mkIf (!cfg.useBootLoader) [
+      in mkIf cfg.directBoot.enable [
         "-kernel \${NIXPKGS_QEMU_KERNEL_${sanitizeShellIdent config.system.name}:-${config.system.build.toplevel}/kernel}"
-        "-initrd ${config.system.build.toplevel}/initrd"
+        "-initrd ${cfg.directBoot.initrd}"
         ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"''
       ])
       (mkIf cfg.useEFIBoot [
         "-drive if=pflash,format=raw,unit=0,readonly=on,file=${cfg.efi.firmware}"
-        "-drive if=pflash,format=raw,unit=1,file=$NIX_EFI_VARS"
+        "-drive if=pflash,format=raw,unit=1,readonly=off,file=$NIX_EFI_VARS"
       ])
       (mkIf (cfg.bios != null) [
         "-bios ${cfg.bios}/bios.bin"
@@ -975,48 +1101,39 @@ in
     ];
 
     virtualisation.qemu.drives = mkMerge [
-      [{
+      (mkIf (cfg.diskImage != null) [{
         name = "root";
         file = ''"$NIX_DISK_IMAGE"'';
         driveExtraOpts.cache = "writeback";
         driveExtraOpts.werror = "report";
-      }]
+        deviceExtraOpts.bootindex = "1";
+        deviceExtraOpts.serial = rootDriveSerialAttr;
+      }])
       (mkIf cfg.useNixStoreImage [{
         name = "nix-store";
         file = ''"$TMPDIR"/store.img'';
-        deviceExtraOpts.bootindex = if cfg.useBootLoader then "3" else "2";
+        deviceExtraOpts.bootindex = "2";
         driveExtraOpts.format = if cfg.writableStore then "qcow2" else "raw";
       }])
-      (mkIf cfg.useBootLoader [
-        # The order of this list determines the device names, see
-        # note [Disk layout with `useBootLoader`].
-        {
-          name = "boot";
-          file = ''"$TMPDIR"/disk.img'';
-          driveExtraOpts.media = "disk";
-          deviceExtraOpts.bootindex = "1";
-        }
-      ])
       (imap0 (idx: _: {
         file = "$(pwd)/empty${toString idx}.qcow2";
         driveExtraOpts.werror = "report";
       }) cfg.emptyDiskImages)
     ];
 
-    # Mount the host filesystem via 9P, and bind-mount the Nix store
-    # of the host into our own filesystem.  We use mkVMOverride to
-    # allow this module to be applied to "normal" NixOS system
-    # configuration, where the regular value for the `fileSystems'
-    # attribute should be disregarded for the purpose of building a VM
-    # test image (since those filesystems don't exist in the VM).
-    fileSystems =
-    let
+    # Use mkVMOverride to enable building test VMs (e.g. via `nixos-rebuild
+    # build-vm`) of a system configuration, where the regular value for the
+    # `fileSystems' attribute should be disregarded (since those filesystems
+    # don't necessarily exist in the VM).
+    fileSystems = mkVMOverride cfg.fileSystems;
+
+    virtualisation.fileSystems = let
       mkSharedDir = tag: share:
         {
           name =
             if tag == "nix-store" && cfg.writableStore
-              then "/nix/.ro-store"
-              else share.target;
+            then "/nix/.ro-store"
+            else share.target;
           value.device = tag;
           value.fsType = "9p";
           value.neededForBoot = true;
@@ -1024,44 +1141,40 @@ in
             [ "trans=virtio" "version=9p2000.L"  "msize=${toString cfg.msize}" ]
             ++ lib.optional (tag == "nix-store") "cache=loose";
         };
-    in
-      mkVMOverride (cfg.fileSystems //
-      optionalAttrs cfg.useDefaultFilesystems {
-        "/".device = cfg.bootDevice;
-        "/".fsType = "ext4";
-        "/".autoFormat = true;
-      } //
-      optionalAttrs config.boot.tmpOnTmpfs {
-        "/tmp" = {
+    in lib.mkMerge [
+      (lib.mapAttrs' mkSharedDir cfg.sharedDirectories)
+      {
+        "/" = lib.mkIf cfg.useDefaultFilesystems (if cfg.diskImage == null then {
+          device = "tmpfs";
+          fsType = "tmpfs";
+        } else {
+          device = cfg.rootDevice;
+          fsType = "ext4";
+        });
+        "/tmp" = lib.mkIf config.boot.tmp.useTmpfs {
           device = "tmpfs";
           fsType = "tmpfs";
           neededForBoot = true;
           # Sync with systemd's tmp.mount;
-          options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmpOnTmpfsSize}" ];
+          options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmp.tmpfsSize}" ];
         };
-      } //
-      optionalAttrs cfg.useNixStoreImage {
-        "/nix/${if cfg.writableStore then ".ro-store" else "store"}" = {
-          device = "${lookupDriveDeviceName "nix-store" cfg.qemu.drives}";
+        "/nix/${if cfg.writableStore then ".ro-store" else "store"}" = lib.mkIf cfg.useNixStoreImage {
+          device = "/dev/disk/by-label/${nixStoreFilesystemLabel}";
           neededForBoot = true;
           options = [ "ro" ];
         };
-      } //
-      optionalAttrs (cfg.writableStore && cfg.writableStoreUseTmpfs) {
-        "/nix/.rw-store" = {
+        "/nix/.rw-store" = lib.mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs) {
           fsType = "tmpfs";
           options = [ "mode=0755" ];
           neededForBoot = true;
         };
-      } //
-      optionalAttrs cfg.useBootLoader {
-        # see note [Disk layout with `useBootLoader`]
-        "/boot" = {
-          device = "${lookupDriveDeviceName "boot" cfg.qemu.drives}2"; # 2 for e.g. `vdb2`, as created in `bootDisk`
+        "/boot" = lib.mkIf (cfg.useBootLoader && cfg.bootPartition != null) {
+          device = cfg.bootPartition;
           fsType = "vfat";
           noCheck = true; # fsck fails on a r/o filesystem
         };
-      } // lib.mapAttrs' mkSharedDir cfg.sharedDirectories);
+      }
+    ];
 
     boot.initrd.systemd = lib.mkIf (config.boot.initrd.systemd.enable && cfg.writableStore) {
       mounts = [{
@@ -1069,18 +1182,20 @@ in
         what = "overlay";
         type = "overlay";
         options = "lowerdir=/sysroot/nix/.ro-store,upperdir=/sysroot/nix/.rw-store/store,workdir=/sysroot/nix/.rw-store/work";
-        wantedBy = ["local-fs.target"];
-        before = ["local-fs.target"];
-        requires = ["sysroot-nix-.ro\\x2dstore.mount" "sysroot-nix-.rw\\x2dstore.mount" "rw-store.service"];
-        after = ["sysroot-nix-.ro\\x2dstore.mount" "sysroot-nix-.rw\\x2dstore.mount" "rw-store.service"];
-        unitConfig.IgnoreOnIsolate = true;
+        wantedBy = ["initrd-fs.target"];
+        before = ["initrd-fs.target"];
+        requires = ["rw-store.service"];
+        after = ["rw-store.service"];
+        unitConfig.RequiresMountsFor = "/sysroot/nix/.ro-store";
       }];
       services.rw-store = {
-        after = ["sysroot-nix-.rw\\x2dstore.mount"];
-        unitConfig.DefaultDependencies = false;
+        unitConfig = {
+          DefaultDependencies = false;
+          RequiresMountsFor = "/sysroot/nix/.rw-store";
+        };
         serviceConfig = {
           Type = "oneshot";
-          ExecStart = "/bin/mkdir -p 0755 /sysroot/nix/.rw-store/store /sysroot/nix/.rw-store/work /sysroot/nix/store";
+          ExecStart = "/bin/mkdir -p -m 0755 /sysroot/nix/.rw-store/store /sysroot/nix/.rw-store/work /sysroot/nix/store";
         };
       };
     };
@@ -1093,14 +1208,14 @@ in
 
     services.qemuGuest.enable = cfg.qemu.guestAgent.enable;
 
-    system.build.vm = cfg.host.pkgs.runCommand "nixos-vm" {
+    system.build.vm = hostPkgs.runCommand "nixos-vm" {
       preferLocalBuild = true;
       meta.mainProgram = "run-${config.system.name}-vm";
     }
       ''
         mkdir -p $out/bin
         ln -s ${config.system.build.toplevel} $out/system
-        ln -s ${cfg.host.pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm
+        ln -s ${hostPkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm
       '';
 
     # When building a regular system configuration, override whatever
diff --git a/nixos/modules/virtualisation/rosetta.nix b/nixos/modules/virtualisation/rosetta.nix
index 109b114d649..ee811b571b8 100644
--- a/nixos/modules/virtualisation/rosetta.nix
+++ b/nixos/modules/virtualisation/rosetta.nix
@@ -50,11 +50,19 @@ in
       }
     ];
 
-    fileSystems."${cfg.mountPoint}" =  {
+    fileSystems."${cfg.mountPoint}" = {
       device = cfg.mountTag;
       fsType = "virtiofs";
     };
 
+
+    nix.settings = {
+      extra-platforms = [ "x86_64-linux" ];
+      extra-sandbox-paths =  [
+        "/run/binfmt"
+        cfg.mountPoint
+      ];
+    };
     boot.binfmt.registrations.rosetta = {
       interpreter = "${cfg.mountPoint}/rosetta";
 
diff --git a/nixos/modules/virtualisation/virtualbox-image.nix b/nixos/modules/virtualisation/virtualbox-image.nix
index 0c095c01ad8..0da217fd1cb 100644
--- a/nixos/modules/virtualisation/virtualbox-image.nix
+++ b/nixos/modules/virtualisation/virtualbox-image.nix
@@ -41,7 +41,7 @@ in {
       };
       vmName = mkOption {
         type = types.str;
-        default = "NixOS ${config.system.nixos.label} (${pkgs.stdenv.hostPlatform.system})";
+        default = "${config.system.nixos.distroName} ${config.system.nixos.label} (${pkgs.stdenv.hostPlatform.system})";
         description = lib.mdDoc ''
           The name of the VirtualBox appliance.
         '';
@@ -81,7 +81,7 @@ in {
       extraDisk = mkOption {
         description = lib.mdDoc ''
           Optional extra disk/hdd configuration.
-          The disk will be an 'ext4' partition on a separate VMDK file.
+          The disk will be an 'ext4' partition on a separate file.
         '';
         default = null;
         example = {
@@ -107,6 +107,46 @@ in {
           };
         });
       };
+      postExportCommands = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          ${pkgs.cot}/bin/cot edit-hardware "$fn" \
+            -v vmx-14 \
+            --nics 2 \
+            --nic-types VMXNET3 \
+            --nic-names 'Nic name' \
+            --nic-networks 'Nic match' \
+            --network-descriptions 'Nic description' \
+            --scsi-subtypes VirtualSCSI
+        '';
+        description = lib.mdDoc ''
+          Extra commands to run after exporting the OVA to `$fn`.
+        '';
+      };
+      storageController = mkOption {
+        type = with types; attrsOf (oneOf [ str int bool (listOf str) ]);
+        example = {
+          name = "SCSI";
+          add = "scsi";
+          portcount = 16;
+          bootable = "on";
+          hostiocache = "on";
+        };
+        default = {
+          name = "SATA";
+          add = "sata";
+          portcount = 4;
+          bootable = "on";
+          hostiocache = "on";
+        };
+        description = lib.mdDoc ''
+          Parameters passed to the VirtualBox appliance. Must have at least
+          `name`.
+
+          Run `VBoxManage storagectl --help` to see more options.
+        '';
+      };
     };
   };
 
@@ -143,8 +183,8 @@ in {
           export HOME=$PWD
           export PATH=${pkgs.virtualbox}/bin:$PATH
 
-          echo "creating VirtualBox pass-through disk wrapper (no copying involved)..."
-          VBoxManage internalcommands createrawvmdk -filename disk.vmdk -rawdisk $diskImage
+          echo "converting image to VirtualBox format..."
+          VBoxManage convertfromraw $diskImage disk.vdi
 
           ${optionalString (cfg.extraDisk != null) ''
             echo "creating extra disk: data-disk.raw"
@@ -156,8 +196,8 @@ in {
               mkpart primary ext4 1MiB -1
             eval $(partx $dataDiskImage -o START,SECTORS --nr 1 --pairs)
             mkfs.ext4 -F -L ${cfg.extraDisk.label} $dataDiskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
-            echo "creating extra disk: data-disk.vmdk"
-            VBoxManage internalcommands createrawvmdk -filename data-disk.vmdk -rawdisk $dataDiskImage
+            echo "creating extra disk: data-disk.vdi"
+            VBoxManage convertfromraw $dataDiskImage data-disk.vdi
           ''}
 
           echo "creating VirtualBox VM..."
@@ -167,18 +207,19 @@ in {
           VBoxManage modifyvm "$vmName" \
             --memory ${toString cfg.memorySize} \
             ${lib.cli.toGNUCommandLineShell { } cfg.params}
-          VBoxManage storagectl "$vmName" --name SATA --add sata --portcount 4 --bootable on --hostiocache on
-          VBoxManage storageattach "$vmName" --storagectl SATA --port 0 --device 0 --type hdd \
-            --medium disk.vmdk
+          VBoxManage storagectl "$vmName" ${lib.cli.toGNUCommandLineShell { } cfg.storageController}
+          VBoxManage storageattach "$vmName" --storagectl ${cfg.storageController.name} --port 0 --device 0 --type hdd \
+            --medium disk.vdi
           ${optionalString (cfg.extraDisk != null) ''
-            VBoxManage storageattach "$vmName" --storagectl SATA --port 1 --device 0 --type hdd \
-            --medium data-disk.vmdk
+            VBoxManage storageattach "$vmName" --storagectl ${cfg.storageController.name} --port 1 --device 0 --type hdd \
+            --medium data-disk.vdi
           ''}
 
           echo "exporting VirtualBox VM..."
           mkdir -p $out
           fn="$out/${cfg.vmFileName}"
           VBoxManage export "$vmName" --output "$fn" --options manifest ${escapeShellArgs cfg.exportParams}
+          ${cfg.postExportCommands}
 
           rm -v $diskImage
 
diff --git a/nixos/modules/virtualisation/xen-domU.nix b/nixos/modules/virtualisation/xen-domU.nix
index c00b984c2ce..ce5a482b114 100644
--- a/nixos/modules/virtualisation/xen-domU.nix
+++ b/nixos/modules/virtualisation/xen-domU.nix
@@ -3,7 +3,6 @@
 { ... }:
 
 {
-  boot.loader.grub.version = 2;
   boot.loader.grub.device = "nodev";
 
   boot.initrd.kernelModules =