about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/development/unit-handling.section.md7
-rw-r--r--nixos/doc/manual/release-notes/rl-2311.section.md60
-rw-r--r--nixos/lib/make-squashfs.nix4
-rw-r--r--nixos/lib/testing/driver.nix7
-rw-r--r--nixos/maintainers/scripts/lxd/lxd-container-image-inner.nix20
-rw-r--r--nixos/maintainers/scripts/lxd/lxd-container-image.nix (renamed from nixos/maintainers/scripts/lxd/lxd-image.nix)6
-rw-r--r--nixos/maintainers/scripts/lxd/lxd-image-inner.nix95
-rw-r--r--nixos/maintainers/scripts/lxd/lxd-virtual-machine-image-inner.nix20
-rw-r--r--nixos/maintainers/scripts/lxd/lxd-virtual-machine-image.nix27
-rw-r--r--nixos/modules/config/update-users-groups.pl28
-rw-r--r--nixos/modules/config/users-groups.nix13
-rw-r--r--nixos/modules/config/zram.nix49
-rw-r--r--nixos/modules/i18n/input-method/uim.nix2
-rw-r--r--nixos/modules/image/amend-repart-definitions.py4
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix1
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix129
-rw-r--r--nixos/modules/module-list.nix8
-rw-r--r--nixos/modules/programs/dconf.nix237
-rw-r--r--nixos/modules/programs/ecryptfs.nix31
-rw-r--r--nixos/modules/programs/htop.nix3
-rw-r--r--nixos/modules/programs/tmux.nix12
-rw-r--r--nixos/modules/programs/zsh/zsh.nix13
-rw-r--r--nixos/modules/security/sudo.nix4
-rw-r--r--nixos/modules/services/audio/goxlr-utility.nix48
-rw-r--r--nixos/modules/services/backup/restic.nix2
-rw-r--r--nixos/modules/services/continuous-integration/github-runner/options.nix8
-rw-r--r--nixos/modules/services/continuous-integration/github-runner/service.nix5
-rw-r--r--nixos/modules/services/databases/influxdb2.nix452
-rw-r--r--nixos/modules/services/editors/emacs.md24
-rw-r--r--nixos/modules/services/editors/emacs.nix17
-rw-r--r--nixos/modules/services/finance/odoo.nix8
-rw-r--r--nixos/modules/services/hardware/auto-cpufreq.nix7
-rw-r--r--nixos/modules/services/hardware/hddfancontrol.nix66
-rw-r--r--nixos/modules/services/logging/graylog.nix4
-rw-r--r--nixos/modules/services/mail/stalwart-mail.nix106
-rw-r--r--nixos/modules/services/matrix/mautrix-telegram.nix1
-rw-r--r--nixos/modules/services/matrix/mautrix-whatsapp.nix99
-rw-r--r--nixos/modules/services/misc/atuin.nix2
-rw-r--r--nixos/modules/services/misc/gitlab.nix7
-rw-r--r--nixos/modules/services/misc/paperless.nix2
-rw-r--r--nixos/modules/services/monitoring/mimir.nix14
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix7
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/mysqld.nix60
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix19
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/unbound.nix84
-rw-r--r--nixos/modules/services/monitoring/vmagent.nix12
-rw-r--r--nixos/modules/services/networking/dae.nix170
-rw-r--r--nixos/modules/services/networking/dnscrypt-proxy2.nix6
-rw-r--r--nixos/modules/services/networking/firewall-nftables.nix9
-rw-r--r--nixos/modules/services/networking/jool.nix281
-rw-r--r--nixos/modules/services/networking/nat-nftables.nix36
-rw-r--r--nixos/modules/services/networking/nftables.nix178
-rw-r--r--nixos/modules/services/networking/nncp.nix131
-rw-r--r--nixos/modules/services/networking/privoxy.nix2
-rw-r--r--nixos/modules/services/networking/tailscale.nix2
-rw-r--r--nixos/modules/services/networking/twingate.nix2
-rw-r--r--nixos/modules/services/security/kanidm.nix6
-rw-r--r--nixos/modules/services/system/zram-generator.nix38
-rw-r--r--nixos/modules/services/web-apps/cloudlog.nix2
-rw-r--r--nixos/modules/services/web-apps/honk.md23
-rw-r--r--nixos/modules/services/web-apps/honk.nix153
-rw-r--r--nixos/modules/services/web-apps/lemmy.nix2
-rw-r--r--nixos/modules/services/web-apps/netbox.nix110
-rw-r--r--nixos/modules/services/web-servers/caddy/default.nix54
-rw-r--r--nixos/modules/services/x11/desktop-managers/budgie.nix3
-rw-r--r--nixos/modules/services/x11/desktop-managers/cinnamon.nix7
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix40
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix1
-rw-r--r--nixos/modules/services/x11/window-managers/default.nix1
-rw-r--r--nixos/modules/services/x11/window-managers/dwm.nix1
-rw-r--r--nixos/modules/services/x11/window-managers/ragnarwm.nix33
-rwxr-xr-xnixos/modules/system/activation/switch-to-configuration.pl24
-rw-r--r--nixos/modules/system/boot/binfmt.nix30
-rw-r--r--nixos/modules/system/boot/loader/grub/install-grub.pl59
-rwxr-xr-xnixos/modules/system/boot/stage-2-init.sh5
-rw-r--r--nixos/modules/system/boot/systemd/initrd.nix16
-rw-r--r--nixos/modules/system/boot/systemd/user.nix4
-rw-r--r--nixos/modules/virtualisation/anbox.nix59
-rw-r--r--nixos/modules/virtualisation/docker.nix4
-rw-r--r--nixos/modules/virtualisation/lxc-container.nix130
-rw-r--r--nixos/modules/virtualisation/lxc-image-metadata.nix104
-rw-r--r--nixos/modules/virtualisation/lxc-instance-common.nix30
-rw-r--r--nixos/modules/virtualisation/lxd-virtual-machine.nix46
-rw-r--r--nixos/modules/virtualisation/lxd.nix129
-rw-r--r--nixos/release-combined.nix5
-rw-r--r--nixos/release.nix51
-rw-r--r--nixos/tests/akkoma.nix5
-rw-r--r--nixos/tests/all-tests.nix17
-rw-r--r--nixos/tests/anbox.nix40
-rw-r--r--nixos/tests/caddy.nix22
-rw-r--r--nixos/tests/common/lxd/config.yaml24
-rw-r--r--nixos/tests/custom-ca.nix4
-rw-r--r--nixos/tests/dae.nix29
-rw-r--r--nixos/tests/dconf.nix34
-rw-r--r--nixos/tests/dolibarr.nix2
-rw-r--r--nixos/tests/hddfancontrol.nix44
-rw-r--r--nixos/tests/honk.nix32
-rw-r--r--nixos/tests/influxdb2.nix193
-rw-r--r--nixos/tests/jool.nix220
-rw-r--r--nixos/tests/listmonk.nix23
-rw-r--r--nixos/tests/lxd-image-server.nix6
-rw-r--r--nixos/tests/lxd/container.nix (renamed from nixos/tests/lxd.nix)47
-rw-r--r--nixos/tests/lxd/default.nix12
-rw-r--r--nixos/tests/lxd/nftables.nix (renamed from nixos/tests/lxd-nftables.nix)7
-rw-r--r--nixos/tests/lxd/preseed.nix71
-rw-r--r--nixos/tests/lxd/ui.nix (renamed from nixos/tests/lxd-ui.nix)2
-rw-r--r--nixos/tests/lxd/virtual-machine.nix64
-rw-r--r--nixos/tests/odoo.nix3
-rw-r--r--nixos/tests/os-prober.nix1
-rw-r--r--nixos/tests/prometheus-exporters.nix40
-rw-r--r--nixos/tests/ragnarwm.nix32
-rw-r--r--nixos/tests/stalwart-mail.nix117
-rw-r--r--nixos/tests/switch-test.nix29
-rw-r--r--nixos/tests/user-activation-scripts.nix3
-rw-r--r--nixos/tests/user-expiry.nix70
-rw-r--r--nixos/tests/virtualbox.nix2
-rw-r--r--nixos/tests/web-apps/netbox-upgrade.nix85
117 files changed, 4232 insertions, 869 deletions
diff --git a/nixos/doc/manual/development/unit-handling.section.md b/nixos/doc/manual/development/unit-handling.section.md
index a7ccb3dbd042..32d44dbfff05 100644
--- a/nixos/doc/manual/development/unit-handling.section.md
+++ b/nixos/doc/manual/development/unit-handling.section.md
@@ -25,8 +25,11 @@ checks:
     since changes in their values are applied by systemd when systemd is
     reloaded.
 
-  - `.mount` units are **reload**ed. These mostly come from the `/etc/fstab`
-    parser.
+  - `.mount` units are **reload**ed if only their `Options` changed. If anything
+    else changed (like `What`), they are **restart**ed unless they are the mount
+    unit for `/` or `/nix` in which case they are reloaded to prevent the system
+    from crashing. Note that this is the case for `.mount` units and not for
+    mounts from `/etc/fstab`. These are explained in [](#sec-switching-systems).
 
   - `.socket` units are currently ignored. This is to be fixed at a later
     point.
diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md
index 31e90c30cf17..20ffcb5ebd87 100644
--- a/nixos/doc/manual/release-notes/rl-2311.section.md
+++ b/nixos/doc/manual/release-notes/rl-2311.section.md
@@ -6,6 +6,8 @@
 
 - Support for WiFi6 (IEEE 802.11ax) and WPA3-SAE-PK was enabled in the `hostapd` package, along with a significant rework of the hostapd module.
 
+- LXD now supports virtual machine instances to complement the existing container support
+
 ## New Services {#sec-release-23.11-new-services}
 
 - [MCHPRS](https://github.com/MCHPR/MCHPRS), a multithreaded Minecraft server built for redstone. Available as [services.mchprs](#opt-services.mchprs.enable).
@@ -20,6 +22,8 @@
 
 - [mautrix-whatsapp](https://docs.mau.fi/bridges/go/whatsapp/index.html) A Matrix-WhatsApp puppeting bridge
 
+- [hddfancontrol](https://github.com/desbma/hddfancontrol), a service to regulate fan speeds based on hard drive temperature. Available as [services.hddfancontrol](#opt-services.hddfancontrol.enable).
+
 - [GoToSocial](https://gotosocial.org/), an ActivityPub social network server, written in Golang. Available as [services.gotosocial](#opt-services.gotosocial.enable).
 
 - [Typesense](https://github.com/typesense/typesense), a fast, typo-tolerant search engine for building delightful search experiences. Available as [services.typesense](#opt-services.typesense.enable).
@@ -28,8 +32,14 @@
 
 - [Anuko Time Tracker](https://github.com/anuko/timetracker), a simple, easy to use, open source time tracking system. Available as [services.anuko-time-tracker](#opt-services.anuko-time-tracker.enable).
 
+- [Prometheus MySQL exporter](https://github.com/prometheus/mysqld_exporter), a MySQL server exporter for Prometheus. Available as [services.prometheus.exporters.mysqld](#opt-services.prometheus.exporters.mysqld.enable).
+
 - [sitespeed-io](https://sitespeed.io), a tool that can generate metrics (timings, diagnostics) for websites. Available as [services.sitespeed-io](#opt-services.sitespeed-io.enable).
 
+- [stalwart-mail](https://stalw.art), an all-in-one email server (SMTP, IMAP, JMAP). Available as [services.stalwart-mail](#opt-services.stalwart-mail.enable).
+
+- [Jool](https://nicmx.github.io/Jool/en/index.html), a kernelspace NAT64 and SIIT implementation, providing translation between IPv4 and IPv6. Available as [networking.jool.enable](#opt-networking.jool.enable).
+
 - [Apache Guacamole](https://guacamole.apache.org/), a cross-platform, clientless remote desktop gateway. Available as [services.guacamole-server](#opt-services.guacamole-server.enable) and [services.guacamole-client](#opt-services.guacamole-client.enable) services.
 
 - [pgBouncer](https://www.pgbouncer.org), a PostgreSQL connection pooler. Available as [services.pgbouncer](#opt-services.pgbouncer.enable).
@@ -44,6 +54,11 @@
 
 - [eris-server](https://codeberg.org/eris/eris-go). [ERIS](https://eris.codeberg.page/) is an encoding for immutable storage and this server provides block exchange as well as content decoding over HTTP and through a FUSE file-system. Available as [services.eris-server](#opt-services.eris-server.enable).
 
+- [Honk](https://humungus.tedunangst.com/r/honk), a complete ActivityPub server with minimal setup and support costs.
+  Available as [services.honk](#opt-services.honk.enable).
+
+- [NNCP](http://www.nncpgo.org/). Added nncp-daemon and nncp-caller services. Configuration is set with [programs.nncp.settings](#opt-programs.nncp.settings) and the daemons are enabled at [services.nncp](#opt-services.nncp.caller.enable).
+
 ## Backward Incompatibilities {#sec-release-23.11-incompatibilities}
 
 - The `boot.loader.raspberryPi` options have been marked deprecated, with intent for removal for NixOS 24.11. They had a limited use-case, and do not work like people expect. They required either very old installs ([before mid-2019](https://github.com/NixOS/nixpkgs/pull/62462)) or customized builds out of scope of the standard and generic AArch64 support. That option set never supported the Raspberry Pi 4 family of devices.
@@ -62,6 +77,10 @@
 
 - `python3.pkgs.fetchPypi` (and `python3Packages.fetchPypi`) has been deprecated in favor of top-level `fetchPypi`.
 
+- `pass` now does not contain `password-store.el`.  Users should get `password-store.el` from Emacs lisp package set `emacs.pkgs.password-store`.
+
+- `mu` now does not install `mu4e` files by default.  Users should get `mu4e` from Emacs lisp package set `emacs.pkgs.mu4e`.
+
 - `mariadb` now defaults to `mariadb_1011` instead of `mariadb_106`, meaning the default version was upgraded from 10.6.x to 10.11.x. See the [upgrade notes](https://mariadb.com/kb/en/upgrading-from-mariadb-10-6-to-mariadb-10-11/) for potential issues.
 
 - `getent` has been moved from `glibc`'s `bin` output to its own dedicated output, reducing closure size for many dependents. Dependents using the `getent` alias should not be affected; others should move from using `glibc.bin` or `getBin glibc` to `getent` (which also improves compatibility with non-glibc platforms).
@@ -77,6 +96,8 @@
 
 - `etcd` has been updated to 3.5, you will want to read the [3.3 to 3.4](https://etcd.io/docs/v3.5/upgrades/upgrade_3_4/) and [3.4 to 3.5](https://etcd.io/docs/v3.5/upgrades/upgrade_3_5/) upgrade guides
 
+- `gitlab` installations created or updated between versions \[15.11.0, 15.11.2] have an incorrect database schema. This will become a problem when upgrading to `gitlab` >=16.2.0. A workaround for affected users can be found in the [GitLab docs](https://docs.gitlab.com/ee/update/versions/gitlab_16_changes.html#undefined-column-error-upgrading-to-162-or-later).
+
 - `consul` has been updated to `1.16.0`. See the [release note](https://github.com/hashicorp/consul/releases/tag/v1.16.0) for more details. Once a new Consul version has started and upgraded its data directory, it generally cannot be downgraded to the previous version.
 
 - `himalaya` has been updated to `0.8.0`, which drops the native TLS support (in favor of Rustls) and add OAuth 2.0 support. See the [release note](https://github.com/soywod/himalaya/releases/tag/v0.8.0) for more details.
@@ -105,10 +126,17 @@
 
 - The ISC DHCP package and corresponding module have been removed, because they are end of life upstream. See https://www.isc.org/blogs/isc-dhcp-eol/ for details and switch to a different DHCP implementation like kea or dnsmasq.
 
+- `prometheus-unbound-exporter` has been replaced by the Let's Encrypt maintained version, since the previous version was archived. This requires some changes to the module configuration, most notable `controlInterface` needs migration
+   towards `unbound.host` and requires either the `tcp://` or `unix://` URI scheme.
+
+- `odoo` now defaults to 16, updated from 15.
+
 - `util-linux` is now supported on Darwin and is no longer an alias to `unixtools`. Use the `unixtools.util-linux` package for access to the Apple variants of the utilities.
 
 - `services.keyd` changed API. Now you can create multiple configuration files.
 
+- `baloo`, the file indexer/search engine used by KDE now has a patch to prevent files from constantly being reindexed when the device ids of the their underlying storage changes. This happens frequently when using btrfs or LVM. The patch has not yet been accepted upstream but it provides a significantly improved experience. When upgrading, reset baloo to get a clean index: `balooctl disable ; balooctl purge ; balooctl enable`.
+
 - `services.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`.
 
 - The `vlock` program from the `kbd` package has been moved into its own package output and should now be referenced explicitly as `kbd.vlock` or replaced with an alternative such as the standalone `vlock` package or `physlock`.
@@ -139,6 +167,8 @@
 
 - The `go-ethereum` package has been updated to v1.12.0. This drops support for proof-of-work. Its GraphQL API now encodes all numeric values as hex strings and the GraphQL UI is updated to version 2.0. The default database has changed from `leveldb` to `pebble` but `leveldb` can be forced with the --db.engine=leveldb flag. The `checkpoint-admin` command was [removed along with trusted checkpoints](https://github.com/ethereum/go-ethereum/pull/27147).
 
+- The `aseprite-unfree` package has been upgraded from 1.2.16.3 to 1.2.40. The free version of aseprite has been dropped because it is EOL and the package attribute now points to the unfree version. A maintained fork of the last free version of Aseprite, named 'LibreSprite', is available in the `libresprite` package.
+
 - The default `kops` version is now 1.27.0 and support for 1.24 and older has been dropped.
 
 - `pharo` has been updated to latest stable (PharoVM 10.0.5), which is compatible with the latest stable and oldstable images (Pharo 10 and 11). The VM in question is the 64bit Spur. The 32bit version has been dropped due to lack of maintenance. The Cog VM has been deleted because it is severily outdated. Finally, the `pharo-launcher` package has been deleted because it was not compatible with the newer VM, and due to lack of maintenance.
@@ -149,11 +179,13 @@
 
 - The `html-proofer` package has been updated from major version 3 to major version 5, which includes [breaking changes](https://github.com/gjtorikian/html-proofer/blob/v5.0.8/UPGRADING.md).
 
+- `kratos` has been updated from 0.10.1 to the first stable version 1.0.0, please read the [0.10.1 to 0.11.0](https://github.com/ory/kratos/releases/tag/v0.11.0), [0.11.0 to 0.11.1](https://github.com/ory/kratos/releases/tag/v0.11.1), [0.11.1 to 0.13.0](https://github.com/ory/kratos/releases/tag/v0.13.0) and [0.13.0 to 1.0.0](https://github.com/ory/kratos/releases/tag/v1.0.0) upgrade guides. The most notable breaking change is the introduction of one-time passwords (`code`) and update of the default recovery strategy from `link` to `code`.
+
 ## Other Notable Changes {#sec-release-23.11-notable-changes}
 
 - The Cinnamon module now enables XDG desktop integration by default. If you are experiencing collisions related to xdg-desktop-portal-gtk you can safely remove `xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ];` from your NixOS configuration.
 
-- GNOME, Pantheon, Cinnamon module no longer forces Qt applications to use Adwaita style since it was buggy and is no longer maintained upstream. If you still want it, you can add the following options to your configuration but it will probably be eventually removed:
+- GNOME, Pantheon, Cinnamon module no longer forces Qt applications to use Adwaita style since it was buggy and is no longer maintained upstream (specifically, Cinnamon now defaults to the gtk2 style instead, following the default in Linux Mint). If you still want it, you can add the following options to your configuration but it will probably be eventually removed:
 
   ```nix
   qt = {
@@ -193,22 +225,48 @@ The module update takes care of the new config syntax and the data itself (user
 
 - `services.prometheus.exporters` has a new exporter to monitor electrical power consumption based on PowercapRAPL sensor called [Scaphandre](https://github.com/hubblo-org/scaphandre), see [#239803](https://github.com/NixOS/nixpkgs/pull/239803) for more details.
 
+- The MariaDB C client library was upgraded from 3.2.x to 3.3.x. It is recomended to review the [upstream release notes](https://mariadb.com/kb/en/mariadb-connector-c-33-release-notes/).
+
 - The module `services.calibre-server` has new options to configure the `host`, `port`, `auth.enable`, `auth.mode` and `auth.userDb` path, see [#216497](https://github.com/NixOS/nixpkgs/pull/216497/) for more details.
 
 - `services.prometheus.exporters` has a new [exporter](https://github.com/hipages/php-fpm_exporter) to monitor PHP-FPM processes, see [#240394](https://github.com/NixOS/nixpkgs/pull/240394) for more details.
 
+- `services.github-runner` / `services.github-runners.<name>` gained the option `nodeRuntimes`. The option defaults to `[ "node20" ]`, i.e., the service supports Node.js 20 GitHub Actions only. The list of Node.js versions accepted by `nodeRuntimes` tracks the versions the upstream GitHub Actions runner supports. See [#249103](https://github.com/NixOS/nixpkgs/pull/249103) for details.
+
 - `programs.gnupg.agent.pinentryFlavor` is now set in `/etc/gnupg/gpg-agent.conf`, and will no longer take precedence over a `pinentry-program` set in `~/.gnupg/gpg-agent.conf`.
 
+- `services.influxdb2` now supports doing an automatic initial setup and provisioning of users, organizations, buckets and authentication tokens, see [#249502](https://github.com/NixOS/nixpkgs/pull/249502) for more details.
+
 - `wrapHelm` now exposes `passthru.pluginsDir` which can be passed to `helmfile`. For convenience, a top-level package `helmfile-wrapped` has been added, which inherits `passthru.pluginsDir` from `kubernetes-helm-wrapped`. See [#217768](https://github.com/NixOS/nixpkgs/issues/217768) for details.
 
 - `boot.initrd.network.udhcp.enable` allows control over dhcp during stage 1 regardless of what `networking.useDHCP` is set to.
 
 - Suricata was upgraded from 6.0 to 7.0 and no longer considers HTTP/2 support as experimental, see [upstream release notes](https://forum.suricata.io/t/suricata-7-0-0-released/3715) for more details.
 
+- `networking.nftables` now has the option `networking.nftables.table.<table>` to create tables
+  and have them be updated atomically, instead of flushing the ruleset.
+
+- `networking.nftables` is no longer flushing all rulesets on every reload.
+  Use `networking.nftables.flushRuleset = true;` to get back the old behaviour.
+
+- The `cawbird` package is dropped from nixpkgs, as it got broken by the Twitter API closing down and has been abandoned upstream.
+
 ## Nixpkgs internals {#sec-release-23.11-nixpkgs-internals}
 
 - The use of `sourceRoot = "source";`, `sourceRoot = "source/subdir";`, and similar lines in package derivations using the default `unpackPhase` is deprecated as it requires `unpackPhase` to always produce a directory named "source". Use `sourceRoot = src.name`, `sourceRoot = "${src.name}/subdir";`, or `setSourceRoot = "sourceRoot=$(echo */subdir)";` or similar instead.
 
+- The `django` alias in the python package set was upgraded to Django 4.x.
+  Applications that consume Django should always pin their python environment
+  to a compatible major version, so they can move at their own pace.
+
+  ```nix
+  python = python3.override {
+    packageOverrides = self: super: {
+      django = super.django_3;
+    };
+  };
+  ```
+
 - The `qemu-vm.nix` module by default now identifies block devices via
   persistent names available in `/dev/disk/by-*`. Because the rootDevice is
   identfied by its filesystem label, it needs to be formatted before the VM is
diff --git a/nixos/lib/make-squashfs.nix b/nixos/lib/make-squashfs.nix
index d1260a48f229..b7c7078b73b1 100644
--- a/nixos/lib/make-squashfs.nix
+++ b/nixos/lib/make-squashfs.nix
@@ -1,4 +1,4 @@
-{ stdenv, squashfsTools, closureInfo
+{ lib, stdenv, squashfsTools, closureInfo
 
 , # The root directory of the squashfs filesystem is filled with the
   # closures of the Nix store paths listed here.
@@ -22,11 +22,13 @@ stdenv.mkDerivation {
       # for nix-store --load-db.
       cp $closureInfo/registration nix-path-registration
 
+    '' + lib.optionalString stdenv.buildPlatform.is32bit ''
       # 64 cores on i686 does not work
       # fails with FATAL ERROR: mangle2:: xz compress failed with error code 5
       if ((NIX_BUILD_CORES > 48)); then
         NIX_BUILD_CORES=48
       fi
+    '' + ''
 
       # Generate the squashfs image.
       mksquashfs nix-path-registration $(cat $closureInfo/store-paths) $out \
diff --git a/nixos/lib/testing/driver.nix b/nixos/lib/testing/driver.nix
index 23574698c062..cc97ca72083f 100644
--- a/nixos/lib/testing/driver.nix
+++ b/nixos/lib/testing/driver.nix
@@ -175,7 +175,12 @@ in
   };
 
   config = {
-    _module.args.hostPkgs = config.hostPkgs;
+    _module.args = {
+      hostPkgs =
+        # Comment is in nixos/modules/misc/nixpkgs.nix
+        lib.mkOverride lib.modules.defaultOverridePriority
+          config.hostPkgs.__splicedPackages;
+    };
 
     driver = withChecks driver;
 
diff --git a/nixos/maintainers/scripts/lxd/lxd-container-image-inner.nix b/nixos/maintainers/scripts/lxd/lxd-container-image-inner.nix
new file mode 100644
index 000000000000..7b743d170bc6
--- /dev/null
+++ b/nixos/maintainers/scripts/lxd/lxd-container-image-inner.nix
@@ -0,0 +1,20 @@
+# Edit this configuration file to define what should be installed on
+# your system.  Help is available in the configuration.nix(5) man page
+# and in the NixOS manual (accessible by running ‘nixos-help’).
+
+{ config, pkgs, lib, ... }:
+
+{
+  imports =
+    [
+      # Include the default lxd configuration.
+      ../../../modules/virtualisation/lxc-container.nix
+      # Include the container-specific autogenerated configuration.
+      ./lxd.nix
+    ];
+
+  networking.useDHCP = false;
+  networking.interfaces.eth0.useDHCP = true;
+
+  system.stateVersion = "21.05"; # Did you read the comment?
+}
diff --git a/nixos/maintainers/scripts/lxd/lxd-image.nix b/nixos/maintainers/scripts/lxd/lxd-container-image.nix
index 07605c5c3120..3bd1320b2b68 100644
--- a/nixos/maintainers/scripts/lxd/lxd-image.nix
+++ b/nixos/maintainers/scripts/lxd/lxd-container-image.nix
@@ -1,4 +1,4 @@
-{ lib, config, pkgs, ... }:
+{ lib, pkgs, ... }:
 
 {
   imports = [
@@ -16,8 +16,8 @@
   system.activationScripts.config = ''
     if [ ! -e /etc/nixos/configuration.nix ]; then
       mkdir -p /etc/nixos
-      cat ${./lxd-image-inner.nix} > /etc/nixos/configuration.nix
-      sed 's|../../../modules/virtualisation/lxc-container.nix|<nixpkgs/nixos/modules/virtualisation/lxc-container.nix>|g' -i /etc/nixos/configuration.nix
+      cat ${./lxd-container-image-inner.nix} > /etc/nixos/configuration.nix
+      ${lib.getExe pkgs.gnused} 's|../../../modules/virtualisation/lxc-container.nix|<nixpkgs/nixos/modules/virtualisation/lxc-container.nix>|g' -i /etc/nixos/configuration.nix
     fi
   '';
 
diff --git a/nixos/maintainers/scripts/lxd/lxd-image-inner.nix b/nixos/maintainers/scripts/lxd/lxd-image-inner.nix
deleted file mode 100644
index c1a9b1aacd18..000000000000
--- a/nixos/maintainers/scripts/lxd/lxd-image-inner.nix
+++ /dev/null
@@ -1,95 +0,0 @@
-# Edit this configuration file to define what should be installed on
-# your system.  Help is available in the configuration.nix(5) man page
-# and in the NixOS manual (accessible by running ‘nixos-help’).
-
-{ config, pkgs, lib, ... }:
-
-{
-  imports =
-    [ # Include the default lxd configuration.
-      ../../../modules/virtualisation/lxc-container.nix
-      # Include the container-specific autogenerated configuration.
-      ./lxd.nix
-    ];
-
-  # networking.hostName = mkForce "nixos"; # Overwrite the hostname.
-  # networking.wireless.enable = true;  # Enables wireless support via wpa_supplicant.
-
-  # Set your time zone.
-  # time.timeZone = "Europe/Amsterdam";
-
-  # The global useDHCP flag is deprecated, therefore explicitly set to false here.
-  # Per-interface useDHCP will be mandatory in the future, so this generated config
-  # replicates the default behaviour.
-  networking.useDHCP = false;
-  networking.interfaces.eth0.useDHCP = true;
-
-  # Configure network proxy if necessary
-  # networking.proxy.default = "http://user:password@proxy:port/";
-  # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
-
-  # Select internationalisation properties.
-  # i18n.defaultLocale = "en_US.UTF-8";
-  # console = {
-  #   font = "Lat2-Terminus16";
-  #   keyMap = "us";
-  # };
-
-  # Enable the X11 windowing system.
-  # services.xserver.enable = true;
-
-  # Configure keymap in X11
-  # services.xserver.layout = "us";
-  # services.xserver.xkbOptions = "eurosign:e";
-
-  # Enable CUPS to print documents.
-  # services.printing.enable = true;
-
-  # Enable sound.
-  # sound.enable = true;
-  # hardware.pulseaudio.enable = true;
-
-  # Enable touchpad support (enabled default in most desktopManager).
-  # services.xserver.libinput.enable = true;
-
-  # Define a user account. Don't forget to set a password with ‘passwd’.
-  # users.users.alice = {
-  #   isNormalUser = true;
-  #   extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
-  # };
-
-  # List packages installed in system profile. To search, run:
-  # $ nix search wget
-  # environment.systemPackages = with pkgs; [
-  #   vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
-  #   wget
-  #   firefox
-  # ];
-
-  # Some programs need SUID wrappers, can be configured further or are
-  # started in user sessions.
-  # programs.mtr.enable = true;
-  # programs.gnupg.agent = {
-  #   enable = true;
-  #   enableSSHSupport = true;
-  # };
-
-  # List services that you want to enable:
-
-  # Enable the OpenSSH daemon.
-  # services.openssh.enable = true;
-
-  # Open ports in the firewall.
-  # networking.firewall.allowedTCPPorts = [ ... ];
-  # networking.firewall.allowedUDPPorts = [ ... ];
-  # Or disable the firewall altogether.
-  # networking.firewall.enable = false;
-
-  # This value determines the NixOS release from which the default
-  # settings for stateful data, like file locations and database versions
-  # on your system were taken. It’s perfectly fine and recommended to leave
-  # this value at the release version of the first install of this system.
-  # Before changing this value read the documentation for this option
-  # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
-  system.stateVersion = "21.05"; # Did you read the comment?
-}
diff --git a/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image-inner.nix b/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image-inner.nix
new file mode 100644
index 000000000000..a8f2c63ac5c6
--- /dev/null
+++ b/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image-inner.nix
@@ -0,0 +1,20 @@
+# Edit this configuration file to define what should be installed on
+# your system.  Help is available in the configuration.nix(5) man page
+# and in the NixOS manual (accessible by running ‘nixos-help’).
+
+{ config, pkgs, lib, ... }:
+
+{
+  imports =
+    [
+      # Include the default lxd configuration.
+      ../../../modules/virtualisation/lxd-virtual-machine.nix
+      # Include the container-specific autogenerated configuration.
+      ./lxd.nix
+    ];
+
+  networking.useDHCP = false;
+  networking.interfaces.eth0.useDHCP = true;
+
+  system.stateVersion = "23.05"; # Did you read the comment?
+}
diff --git a/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image.nix b/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image.nix
new file mode 100644
index 000000000000..eb0d9217d402
--- /dev/null
+++ b/nixos/maintainers/scripts/lxd/lxd-virtual-machine-image.nix
@@ -0,0 +1,27 @@
+{ lib, pkgs, ... }:
+
+{
+  imports = [
+    ../../../modules/virtualisation/lxd-virtual-machine.nix
+  ];
+
+  virtualisation.lxc.templates.nix = {
+    enable = true;
+    target = "/etc/nixos/lxd.nix";
+    template = ./nix.tpl;
+    when = ["create" "copy"];
+  };
+
+  # copy the config for nixos-rebuild
+  system.activationScripts.config = ''
+    if [ ! -e /etc/nixos/configuration.nix ]; then
+      mkdir -p /etc/nixos
+      cat ${./lxd-virtual-machine-image-inner.nix} > /etc/nixos/configuration.nix
+      ${lib.getExe pkgs.gnused} 's|../../../modules/virtualisation/lxd-virtual-machine.nix|<nixpkgs/nixos/modules/virtualisation/lxd-virtual-machine.nix>|g' -i /etc/nixos/configuration.nix
+    fi
+  '';
+
+  # Network
+  networking.useDHCP = false;
+  networking.interfaces.enp5s0.useDHCP = true;
+}
diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl
index 75c343523e27..3785ba8c5fb8 100644
--- a/nixos/modules/config/update-users-groups.pl
+++ b/nixos/modules/config/update-users-groups.pl
@@ -4,6 +4,7 @@ use File::Path qw(make_path);
 use File::Slurp;
 use Getopt::Long;
 use JSON;
+use Time::Piece;
 
 # Keep track of deleted uids and gids.
 my $uidMapFile = "/var/lib/nixos/uid-map";
@@ -22,6 +23,13 @@ sub updateFile {
     write_file($path, { atomic => 1, binmode => ':utf8', perms => $perms // 0644 }, $contents) or die;
 }
 
+# Converts an ISO date to number of days since 1970-01-01
+sub dateToDays {
+    my ($date) = @_;
+    my $time = Time::Piece->strptime($date, "%Y-%m-%d");
+    return $time->epoch / 60 / 60 / 24;
+}
+
 sub nscdInvalidate {
     system("nscd", "--invalidate", $_[0]) unless $is_dry;
 }
@@ -285,22 +293,26 @@ my %shadowSeen;
 
 foreach my $line (-f "/etc/shadow" ? read_file("/etc/shadow", { binmode => ":utf8" }) : ()) {
     chomp $line;
-    my ($name, $hashedPassword, @rest) = split(':', $line, -9);
-    my $u = $usersOut{$name};;
+    # struct name copied from `man 3 shadow`
+    my ($sp_namp, $sp_pwdp, $sp_lstch, $sp_min, $sp_max, $sp_warn, $sp_inact, $sp_expire, $sp_flag) = split(':', $line, -9);
+    my $u = $usersOut{$sp_namp};;
     next if !defined $u;
-    $hashedPassword = "!" if !$spec->{mutableUsers};
-    $hashedPassword = $u->{hashedPassword} if defined $u->{hashedPassword} && !$spec->{mutableUsers}; # FIXME
-    chomp $hashedPassword;
-    push @shadowNew, join(":", $name, $hashedPassword, @rest) . "\n";
-    $shadowSeen{$name} = 1;
+    $sp_pwdp = "!" if !$spec->{mutableUsers};
+    $sp_pwdp = $u->{hashedPassword} if defined $u->{hashedPassword} && !$spec->{mutableUsers}; # FIXME
+    $sp_expire = dateToDays($u->{expires}) if defined $u->{expires};
+    chomp $sp_pwdp;
+    push @shadowNew, join(":", $sp_namp, $sp_pwdp, $sp_lstch, $sp_min, $sp_max, $sp_warn, $sp_inact, $sp_expire, $sp_flag) . "\n";
+    $shadowSeen{$sp_namp} = 1;
 }
 
 foreach my $u (values %usersOut) {
     next if defined $shadowSeen{$u->{name}};
     my $hashedPassword = "!";
     $hashedPassword = $u->{hashedPassword} if defined $u->{hashedPassword};
+    my $expires = "";
+    $expires = dateToDays($u->{expires}) if defined $u->{expires};
     # FIXME: set correct value for sp_lstchg.
-    push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::::") . "\n";
+    push @shadowNew, join(":", $u->{name}, $hashedPassword, "1::::", $expires, "") . "\n";
 }
 
 updateFile("/etc/shadow", \@shadowNew, 0640);
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
index 4c9e286ea5fd..684b4bc8fbcc 100644
--- a/nixos/modules/config/users-groups.nix
+++ b/nixos/modules/config/users-groups.nix
@@ -311,6 +311,17 @@ let
         '';
       };
 
+      expires = mkOption {
+        type = types.nullOr (types.strMatching "[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}");
+        default = null;
+        description = lib.mdDoc ''
+          Set the date on which the user's account will no longer be
+          accessible. The date is expressed in the format YYYY-MM-DD, or null
+          to disable the expiry.
+          A user whose account is locked must contact the system
+          administrator before being able to use the system again.
+        '';
+      };
     };
 
     config = mkMerge
@@ -438,7 +449,7 @@ let
           name uid group description home homeMode createHome isSystemUser
           password passwordFile hashedPassword
           autoSubUidGidRange subUidRanges subGidRanges
-          initialPassword initialHashedPassword;
+          initialPassword initialHashedPassword expires;
         shell = utils.toShellPath u.shell;
       }) cfg.users;
     groups = attrValues cfg.groups;
diff --git a/nixos/modules/config/zram.nix b/nixos/modules/config/zram.nix
index 991387ea9b2b..ec8b4ed6e931 100644
--- a/nixos/modules/config/zram.nix
+++ b/nixos/modules/config/zram.nix
@@ -105,36 +105,25 @@ in
       }
     ];
 
-
-    system.requiredKernelConfig = with config.lib.kernelConfig; [
-      (isModule "ZRAM")
-    ];
-
-    # Disabling this for the moment, as it would create and mkswap devices twice,
-    # once in stage 2 boot, and again when the zram-reloader service starts.
-    # boot.kernelModules = [ "zram" ];
-
-    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));
+    services.zram-generator.enable = true;
+
+    services.zram-generator.settings = 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/i18n/input-method/uim.nix b/nixos/modules/i18n/input-method/uim.nix
index 9491ab2640fc..7225783b2a6f 100644
--- a/nixos/modules/i18n/input-method/uim.nix
+++ b/nixos/modules/i18n/input-method/uim.nix
@@ -10,7 +10,7 @@ in
 
     i18n.inputMethod.uim = {
       toolbar = mkOption {
-        type    = types.enum [ "gtk" "gtk3" "gtk-systray" "gtk3-systray" "qt4" ];
+        type    = types.enum [ "gtk" "gtk3" "gtk-systray" "gtk3-systray" "qt5" ];
         default = "gtk";
         example = "gtk-systray";
         description = lib.mdDoc ''
diff --git a/nixos/modules/image/amend-repart-definitions.py b/nixos/modules/image/amend-repart-definitions.py
index 52f10303eb5e..fa9b1544ae85 100644
--- a/nixos/modules/image/amend-repart-definitions.py
+++ b/nixos/modules/image/amend-repart-definitions.py
@@ -53,7 +53,7 @@ def add_closure_to_definition(
 
             source = Path(line.strip())
             target = str(source.relative_to("/nix/store/"))
-            target = f":{target}" if strip_nix_store_prefix else ""
+            target = f":/{target}" if strip_nix_store_prefix else ""
 
             copy_files_lines.append(f"CopyFiles={source}{target}\n")
 
@@ -102,7 +102,7 @@ def main() -> None:
         add_contents_to_definition(definition, contents)
 
         closure = config.get("closure")
-        strip_nix_store_prefix = config.get("stripStorePaths")
+        strip_nix_store_prefix = config.get("stripNixStorePrefix")
         add_closure_to_definition(definition, closure, strip_nix_store_prefix)
 
     print(target_dir.absolute())
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 ea8056ff870c..573b31b439c2 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
@@ -6,7 +6,6 @@
   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/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index c430048d6598..0b5135c088ea 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -24,6 +24,9 @@ let
         # Name appended to menuentry defaults to params if no specific name given.
         option.name or (optionalString (option ? params) "(${option.params})")
         }' ${optionalString (option ? class) " --class ${option.class}"} {
+          # Fallback to UEFI console for boot, efifb sometimes has difficulties.
+          terminal_output console
+
           linux ${defaults.image} \''${isoboot} ${defaults.params} ${
             option.params or ""
           }
@@ -185,33 +188,25 @@ let
       # So instead we'll list a lot of possibly valid modes :/
       #"3840x2160"
       #"2560x1440"
+      "1920x1200"
       "1920x1080"
       "1366x768"
+      "1280x800"
       "1280x720"
+      "1200x1920"
       "1024x768"
+      "800x1280"
       "800x600"
       "auto"
     ]}
 
-    # Fonts can be loaded?
-    # (This font is assumed to always be provided as a fallback by NixOS)
-    if loadfont (\$root)/EFI/boot/unicode.pf2; then
-      set with_fonts=true
-    fi
-    if [ "\$textmode" != "true" -a "\$with_fonts" == "true" ]; then
-      # Use graphical term, it can be either with background image or a theme.
-      # input is "console", while output is "gfxterm".
-      # This enables "serial" input and output only when possible.
-      # Otherwise the failure mode is to not even enable gfxterm.
-      if test "\$with_serial" == "yes"; then
-        terminal_output gfxterm serial
-        terminal_input  console serial
-      else
-        terminal_output gfxterm
-        terminal_input  console
-      fi
+    if [ "\$textmode" == "false" ]; then
+      terminal_output gfxterm
+      terminal_input  console
     else
-      # Sets colors for the non-graphical term.
+      terminal_output console
+      terminal_input  console
+      # Sets colors for console term.
       set menu_color_normal=cyan/blue
       set menu_color_highlight=white/blue
     fi
@@ -250,18 +245,58 @@ let
     touch $out/EFI/nixos-installer-image
 
     # ALWAYS required modules.
-    MODULES="fat iso9660 part_gpt part_msdos \
-             normal boot linux configfile loopback chain halt \
-             efifwsetup efi_gop \
-             ls search search_label search_fs_uuid search_fs_file \
-             gfxmenu gfxterm gfxterm_background gfxterm_menu test all_video loadenv \
-             exfat ext2 ntfs btrfs hfsplus udf \
-             videoinfo png \
-             echo serial \
-            "
+    MODULES=(
+      # Basic modules for filesystems and partition schemes
+      "fat"
+      "iso9660"
+      "part_gpt"
+      "part_msdos"
+
+      # Basic stuff
+      "normal"
+      "boot"
+      "linux"
+      "configfile"
+      "loopback"
+      "chain"
+      "halt"
+
+      # Allows rebooting into firmware setup interface
+      "efifwsetup"
+
+      # EFI Graphics Output Protocol
+      "efi_gop"
+
+      # User commands
+      "ls"
+
+      # System commands
+      "search"
+      "search_label"
+      "search_fs_uuid"
+      "search_fs_file"
+      "echo"
+
+      # We're not using it anymore, but we'll leave it in so it can be used
+      # by user, with the console using "C"
+      "serial"
+
+      # Graphical mode stuff
+      "gfxmenu"
+      "gfxterm"
+      "gfxterm_background"
+      "gfxterm_menu"
+      "test"
+      "loadenv"
+      "all_video"
+      "videoinfo"
+
+      # File types for graphical mode
+      "png"
+    )
 
     echo "Building GRUB with modules:"
-    for mod in $MODULES; do
+    for mod in ''${MODULES[@]}; do
       echo " - $mod"
     done
 
@@ -270,31 +305,27 @@ let
     for mod in efi_uga; do
       if [ -f ${grubPkgs.grub2_efi}/lib/grub/${grubPkgs.grub2_efi.grubTarget}/$mod.mod ]; then
         echo " - $mod"
-        MODULES+=" $mod"
+        MODULES+=("$mod")
       fi
     done
 
     # Make our own efi program, we can't rely on "grub-install" since it seems to
     # probe for devices, even with --skip-fs-probe.
-    grub-mkimage --directory=${grubPkgs.grub2_efi}/lib/grub/${grubPkgs.grub2_efi.grubTarget} -o $out/EFI/boot/boot${targetArch}.efi -p /EFI/boot -O ${grubPkgs.grub2_efi.grubTarget} \
-      $MODULES
+    grub-mkimage \
+      --directory=${grubPkgs.grub2_efi}/lib/grub/${grubPkgs.grub2_efi.grubTarget} \
+      -o $out/EFI/boot/boot${targetArch}.efi \
+      -p /EFI/boot \
+      -O ${grubPkgs.grub2_efi.grubTarget} \
+      ''${MODULES[@]}
     cp ${grubPkgs.grub2_efi}/share/grub/unicode.pf2 $out/EFI/boot/
 
     cat <<EOF > $out/EFI/boot/grub.cfg
 
-    set with_fonts=false
-    set textmode=${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
-    # This uses the defaults, and makes the serial terminal available.
-    set with_serial=no
-    if serial; then set with_serial=yes ;fi
-    export with_serial
-    clear
+    set textmode=${boolToString (config.isoImage.forceTextMode)}
     set timeout=${toString grubEfiTimeout}
 
-    # This message will only be viewable when "gfxterm" is not used.
+    clear
+    # This message will only be viewable on the default (UEFI) console.
     echo ""
     echo "Loading graphical boot menu..."
     echo ""
@@ -306,7 +337,7 @@ let
     hiddenentry 'Text mode' --hotkey 't' {
       loadfont (\$root)/EFI/boot/unicode.pf2
       set textmode=true
-      terminal_output gfxterm console
+      terminal_output console
     }
     hiddenentry 'GUI mode' --hotkey 'g' {
       $(find ${config.isoImage.grubTheme} -iname '*.pf2' -printf "loadfont (\$root)/EFI/boot/grub-theme/%P\n")
@@ -400,6 +431,8 @@ let
     }
     EOF
 
+    grub-script-check $out/EFI/boot/grub.cfg
+
     ${refind}
   '';
 
@@ -658,13 +691,17 @@ in
       '';
     };
 
-    isoImage.graphicalGrub = mkOption {
+    isoImage.forceTextMode = mkOption {
       default = false;
       type = types.bool;
       example = true;
       description = lib.mdDoc ''
-        Whether to use textmode or graphical grub.
-        false means we use textmode grub.
+        Whether to use text mode instead of graphical grub.
+        A value of `true` means graphical mode is not tried to be used.
+
+        This is useful for validating that graphics mode usage is not at the root cause of a problem with the iso image.
+
+        If text mode is required off-handedly (e.g. for serial use) you can use the `T` key, after being prompted, to use text mode for the current boot.
       '';
     };
 
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 0fff271c8684..d650e5ec76b4 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -163,6 +163,7 @@
   ./programs/direnv.nix
   ./programs/dmrconfig.nix
   ./programs/droidcam.nix
+  ./programs/ecryptfs.nix
   ./programs/environment.nix
   ./programs/evince.nix
   ./programs/extra-container.nix
@@ -319,6 +320,7 @@
   ./services/audio/botamusique.nix
   ./services/audio/gmediarender.nix
   ./services/audio/gonic.nix
+  ./services/audio/goxlr-utility.nix
   ./services/audio/hqplayerd.nix
   ./services/audio/icecast.nix
   ./services/audio/jack.nix
@@ -504,6 +506,7 @@
   ./services/hardware/fancontrol.nix
   ./services/hardware/freefall.nix
   ./services/hardware/fwupd.nix
+  ./services/hardware/hddfancontrol.nix
   ./services/hardware/illum.nix
   ./services/hardware/interception-tools.nix
   ./services/hardware/irqbalance.nix
@@ -592,6 +595,7 @@
   ./services/mail/rss2email.nix
   ./services/mail/schleuder.nix
   ./services/mail/spamassassin.nix
+  ./services/mail/stalwart-mail.nix
   ./services/mail/sympa.nix
   ./services/mail/zeyple.nix
   ./services/matrix/appservice-discord.nix
@@ -928,6 +932,7 @@
   ./services/networking/jibri/default.nix
   ./services/networking/jicofo.nix
   ./services/networking/jitsi-videobridge.nix
+  ./services/networking/jool.nix
   ./services/networking/kea.nix
   ./services/networking/keepalived/default.nix
   ./services/networking/keybase.nix
@@ -978,6 +983,7 @@
   ./services/networking/nix-serve.nix
   ./services/networking/nix-store-gcs-proxy.nix
   ./services/networking/nixops-dns.nix
+  ./services/networking/nncp.nix
   ./services/networking/nntp-proxy.nix
   ./services/networking/nomad.nix
   ./services/networking/nsd.nix
@@ -1169,6 +1175,7 @@
   ./services/system/self-deploy.nix
   ./services/system/systembus-notify.nix
   ./services/system/uptimed.nix
+  ./services/system/zram-generator.nix
   ./services/torrent/deluge.nix
   ./services/torrent/flexget.nix
   ./services/torrent/magnetico.nix
@@ -1223,6 +1230,7 @@
   ./services/web-apps/healthchecks.nix
   ./services/web-apps/hedgedoc.nix
   ./services/web-apps/hledger-web.nix
+  ./services/web-apps/honk.nix
   ./services/web-apps/icingaweb2/icingaweb2.nix
   ./services/web-apps/icingaweb2/module-monitoring.nix
   ./services/web-apps/invidious.nix
diff --git a/nixos/modules/programs/dconf.nix b/nixos/modules/programs/dconf.nix
index 7261a143528f..cf53658c4fad 100644
--- a/nixos/modules/programs/dconf.nix
+++ b/nixos/modules/programs/dconf.nix
@@ -1,55 +1,217 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   cfg = config.programs.dconf;
-  cfgDir = pkgs.symlinkJoin {
-    name = "dconf-system-config";
-    paths = map (x: "${x}/etc/dconf") cfg.packages;
-    postBuild = ''
-      mkdir -p $out/profile
-      mkdir -p $out/db
-    '' + (
-      concatStringsSep "\n" (
-        mapAttrsToList (
-          name: path: ''
-            ln -s ${path} $out/profile/${name}
-          ''
-        ) cfg.profiles
-      )
-    ) + ''
-      ${pkgs.dconf}/bin/dconf update $out/db
-    '';
+
+  # Compile keyfiles to dconf DB
+  compileDconfDb = dir: pkgs.runCommand "dconf-db"
+    {
+      nativeBuildInputs = [ (lib.getBin pkgs.dconf) ];
+    } "dconf compile $out ${dir}";
+
+  # Check if dconf keyfiles are valid
+  checkDconfKeyfiles = dir: pkgs.runCommand "check-dconf-keyfiles"
+    {
+      nativeBuildInputs = [ (lib.getBin pkgs.dconf) ];
+    } ''
+    if [[ -f ${dir} ]]; then
+      echo "dconf keyfiles should be a directory but a file is provided: ${dir}"
+      exit 1
+    fi
+
+    dconf compile db ${dir} || (
+      echo "The dconf keyfiles are invalid: ${dir}"
+      exit 1
+    )
+    cp -R ${dir} $out
+  '';
+
+  mkAllLocks = settings: lib.flatten (
+    lib.mapAttrsToList (k: v: lib.mapAttrsToList (k': _: "/${k}/${k'}") v) settings);
+
+  # Generate dconf DB from dconfDatabase and keyfiles
+  mkDconfDb = val: compileDconfDb (pkgs.symlinkJoin {
+    name = "nixos-generated-dconf-keyfiles";
+    paths = [
+      (pkgs.writeTextDir "nixos-generated-dconf-keyfiles" (lib.generators.toDconfINI val.settings))
+      (pkgs.writeTextDir "locks/nixos-generated-dconf-locks" (lib.concatStringsSep "\n"
+        (if val.lockAll then mkAllLocks val.settings else val.locks)
+      ))
+    ] ++ (map checkDconfKeyfiles val.keyfiles);
+  });
+
+  # Check if a dconf DB file is valid. The dconf cli doesn't return 1 when it can't
+  # open the database file so we have to check if the output is empty.
+  checkDconfDb = file: pkgs.runCommand "check-dconf-db"
+    {
+      nativeBuildInputs = [ (lib.getBin pkgs.dconf) ];
+    } ''
+    if [[ -d ${file} ]]; then
+      echo "dconf DB should be a file but a directory is provided: ${file}"
+      exit 1
+    fi
+
+    echo "file-db:${file}" > profile
+    DCONF_PROFILE=$(pwd)/profile dconf dump / > output 2> error
+    if [[ ! -s output ]] && [[ -s error ]]; then
+      cat error
+      echo "The dconf DB file is invalid: ${file}"
+      exit 1
+    fi
+
+    cp ${file} $out
+  '';
+
+  # Generate dconf profile
+  mkDconfProfile = name: value:
+    if lib.isDerivation value || lib.isPath value then
+      pkgs.runCommand "dconf-profile" { } ''
+        if [[ -d ${value} ]]; then
+          echo "Dconf profile should be a file but a directory is provided."
+          exit 1
+        fi
+        mkdir -p $out/etc/dconf/profile/
+        cp ${value} $out/etc/dconf/profile/${name}
+      ''
+    else
+      pkgs.writeTextDir "etc/dconf/profile/${name}" (
+        lib.concatMapStrings (x: "${x}\n") ((
+          lib.optional value.enableUserDb "user-db:user"
+        ) ++ (
+          map
+            (value:
+              let
+                db = if lib.isAttrs value && !lib.isDerivation value then mkDconfDb value else checkDconfDb value;
+              in
+              "file-db:${db}")
+            value.databases
+        ))
+      );
+
+  dconfDatabase = with lib.types; submodule {
+    options = {
+      keyfiles = lib.mkOption {
+        type = listOf (oneOf [ path package ]);
+        default = [ ];
+        description = lib.mdDoc "A list of dconf keyfile directories.";
+      };
+      settings = lib.mkOption {
+        type = attrs;
+        default = { };
+        description = lib.mdDoc "An attrset used to generate dconf keyfile.";
+        example = literalExpression ''
+          with lib.gvariant;
+          {
+            "com/raggesilver/BlackBox" = {
+              scrollback-lines = mkUint32 10000;
+              theme-dark = "Tommorow Night";
+            };
+          }
+        '';
+      };
+      locks = lib.mkOption {
+        type = with lib.types; listOf str;
+        default = [ ];
+        description = lib.mdDoc ''
+          A list of dconf keys to be lockdown. This doesn't take effect if `lockAll`
+          is set.
+        '';
+        example = literalExpression ''
+          [ "/org/gnome/desktop/background/picture-uri" ]
+        '';
+      };
+      lockAll = lib.mkOption {
+        type = lib.types.bool;
+        default = false;
+        description = lib.mdDoc "Lockdown all dconf keys in `settings`.";
+      };
+    };
+  };
+
+  dconfProfile = with lib.types; submodule {
+    options = {
+      enableUserDb = lib.mkOption {
+        type = bool;
+        default = true;
+        description = lib.mdDoc "Add `user-db:user` at the beginning of the profile.";
+      };
+
+      databases = lib.mkOption {
+        type = with lib.types; listOf (oneOf [
+          path
+          package
+          dconfDatabase
+        ]);
+        default = [ ];
+        description = lib.mdDoc ''
+          List of data sources for the profile. An element can be an attrset,
+          or the path of an already compiled database. Each element is converted
+          to a file-db.
+
+          A key is searched from up to down and the first result takes the
+          priority. If a lock for a particular key is installed then the value from
+          the last database in the profile where the key is locked will be used.
+          This can be used to enforce mandatory settings.
+        '';
+      };
+    };
   };
+
 in
 {
-  ###### interface
-
   options = {
     programs.dconf = {
-      enable = mkEnableOption (lib.mdDoc "dconf");
+      enable = lib.mkEnableOption (lib.mdDoc "dconf");
 
-      profiles = mkOption {
-        type = types.attrsOf types.path;
-        default = {};
-        description = lib.mdDoc "Set of dconf profile files, installed at {file}`/etc/dconf/profiles/«name»`.";
-        internal = true;
+      profiles = lib.mkOption {
+        type = with lib.types; attrsOf (oneOf [
+          path
+          package
+          dconfProfile
+        ]);
+        default = { };
+        description = lib.mdDoc ''
+          Attrset of dconf profiles. By default the `user` profile is used which
+          ends up in `/etc/dconf/profile/user`.
+        '';
+        example = lib.literalExpression ''
+          {
+            # A "user" profile with a database
+            user.databases = [
+              {
+                settings = { };
+              }
+            ];
+            # A "bar" profile from a package
+            bar = pkgs.bar-dconf-profile;
+            # A "foo" profile from a path
+            foo = ''${./foo}
+          };
+        '';
       };
 
-      packages = mkOption {
-        type = types.listOf types.package;
-        default = [];
+      packages = lib.mkOption {
+        type = lib.types.listOf lib.types.package;
+        default = [ ];
         description = lib.mdDoc "A list of packages which provide dconf profiles and databases in {file}`/etc/dconf`.";
       };
     };
   };
 
-  ###### implementation
+  config = lib.mkIf (cfg.profiles != { } || cfg.enable) {
+    programs.dconf.packages = lib.mapAttrsToList mkDconfProfile cfg.profiles;
 
-  config = mkIf (cfg.profiles != {} || cfg.enable) {
-    environment.etc.dconf = mkIf (cfg.profiles != {} || cfg.packages != []) {
-      source = cfgDir;
+    environment.etc.dconf = lib.mkIf (cfg.packages != [ ]) {
+      source = pkgs.symlinkJoin {
+        name = "dconf-system-config";
+        paths = map (x: "${x}/etc/dconf") cfg.packages;
+        nativeBuildInputs = [ (lib.getBin pkgs.dconf) ];
+        postBuild = ''
+          if test -d $out/db; then
+            dconf update $out/db
+          fi
+        '';
+      };
     };
 
     services.dbus.packages = [ pkgs.dconf ];
@@ -59,8 +221,9 @@ in
     # For dconf executable
     environment.systemPackages = [ pkgs.dconf ];
 
-    # Needed for unwrapped applications
-    environment.sessionVariables.GIO_EXTRA_MODULES = mkIf cfg.enable [ "${pkgs.dconf.lib}/lib/gio/modules" ];
+    environment.sessionVariables = lib.mkIf cfg.enable {
+      # Needed for unwrapped applications
+      GIO_EXTRA_MODULES = [ "${pkgs.dconf.lib}/lib/gio/modules" ];
+    };
   };
-
 }
diff --git a/nixos/modules/programs/ecryptfs.nix b/nixos/modules/programs/ecryptfs.nix
new file mode 100644
index 000000000000..63c1a3ad4419
--- /dev/null
+++ b/nixos/modules/programs/ecryptfs.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.ecryptfs;
+
+in {
+  options.programs.ecryptfs = {
+    enable = mkEnableOption (lib.mdDoc "ecryptfs setuid mount wrappers");
+  };
+
+  config = mkIf cfg.enable {
+    security.wrappers = {
+
+      "mount.ecryptfs_private" = {
+        setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${lib.getBin pkgs.ecryptfs}/bin/mount.ecryptfs_private";
+      };
+      "umount.ecryptfs_private" = {
+        setuid = true;
+        owner = "root";
+        group = "root";
+        source = "${lib.getBin pkgs.ecryptfs}/bin/umount.ecryptfs_private";
+      };
+
+    };
+  };
+}
diff --git a/nixos/modules/programs/htop.nix b/nixos/modules/programs/htop.nix
index 2682ced490ca..777ea709836e 100644
--- a/nixos/modules/programs/htop.nix
+++ b/nixos/modules/programs/htop.nix
@@ -9,7 +9,8 @@ let
   fmt = value:
     if isList value then concatStringsSep " " (map fmt value) else
     if isString value then value else
-    if isBool value || isInt value then toString value else
+    if isBool value then if value then "1" else "0" else
+    if isInt value then toString value else
     throw "Unrecognized type ${typeOf value} in htop settings";
 
 in
diff --git a/nixos/modules/programs/tmux.nix b/nixos/modules/programs/tmux.nix
index 4f452f1d7f9b..0d1c7c9cdf0f 100644
--- a/nixos/modules/programs/tmux.nix
+++ b/nixos/modules/programs/tmux.nix
@@ -52,6 +52,8 @@ let
     set  -s escape-time       ${toString cfg.escapeTime}
     set  -g history-limit     ${toString cfg.historyLimit}
 
+    ${cfg.extraConfigBeforePlugins}
+
     ${lib.optionalString (cfg.plugins != []) ''
     # Run plugins
     ${lib.concatMapStringsSep "\n" (x: "run-shell ${x.rtp}") cfg.plugins}
@@ -108,10 +110,18 @@ in {
         description = lib.mdDoc "Time in milliseconds for which tmux waits after an escape is input.";
       };
 
+      extraConfigBeforePlugins = mkOption {
+        default = "";
+        description = lib.mdDoc ''
+          Additional contents of /etc/tmux.conf, to be run before sourcing plugins.
+        '';
+        type = types.lines;
+      };
+
       extraConfig = mkOption {
         default = "";
         description = lib.mdDoc ''
-          Additional contents of /etc/tmux.conf
+          Additional contents of /etc/tmux.conf, to be run after sourcing plugins.
         '';
         type = types.lines;
       };
diff --git a/nixos/modules/programs/zsh/zsh.nix b/nixos/modules/programs/zsh/zsh.nix
index 6bb21cb3ef66..cad639f299c8 100644
--- a/nixos/modules/programs/zsh/zsh.nix
+++ b/nixos/modules/programs/zsh/zsh.nix
@@ -159,6 +159,14 @@ in
         type = types.bool;
       };
 
+      enableLsColors = mkOption {
+        default = true;
+        description = lib.mdDoc ''
+          Enable extra colors in directory listings (used by `ls` and `tree`).
+        '';
+        type = types.bool;
+      };
+
     };
 
   };
@@ -263,6 +271,11 @@ in
 
         ${cfg.interactiveShellInit}
 
+        ${optionalString cfg.enableLsColors ''
+          # Extra colors for directory listings.
+          eval "$(${pkgs.coreutils}/bin/dircolors -b)"
+        ''}
+
         # Setup aliases.
         ${zshAliases}
 
diff --git a/nixos/modules/security/sudo.nix b/nixos/modules/security/sudo.nix
index 9ac91bd0d368..d225442773c6 100644
--- a/nixos/modules/security/sudo.nix
+++ b/nixos/modules/security/sudo.nix
@@ -192,6 +192,10 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.package.pname != "sudo-rs";
+        message = "The NixOS `sudo` module does not work with `sudo-rs` yet."; }
+    ];
 
     # We `mkOrder 600` so that the default rule shows up first, but there is
     # still enough room for a user to `mkBefore` it.
diff --git a/nixos/modules/services/audio/goxlr-utility.nix b/nixos/modules/services/audio/goxlr-utility.nix
new file mode 100644
index 000000000000..b719de875c7f
--- /dev/null
+++ b/nixos/modules/services/audio/goxlr-utility.nix
@@ -0,0 +1,48 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.goxlr-utility;
+in
+
+with lib;
+{
+
+  options = {
+    services.goxlr-utility = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = lib.mdDoc ''
+          Whether to enable goxlr-utility for controlling your TC-Helicon GoXLR or GoXLR Mini
+        '';
+      };
+      package = mkPackageOptionMD pkgs "goxlr-utility" { };
+      autoStart.xdg = mkOption {
+        default = true;
+        type = with types; bool;
+        description = lib.mdDoc ''
+          Start the daemon automatically using XDG autostart.
+          Sets `xdg.autostart.enable = true` if not already enabled.
+        '';
+      };
+    };
+  };
+
+  config = mkIf config.services.goxlr-utility.enable
+    {
+      services.udev.packages = [ cfg.package ];
+
+      xdg.autostart.enable = mkIf cfg.autoStart.xdg true;
+      environment.systemPackages = mkIf cfg.autoStart.xdg
+        [
+          cfg.package
+          (pkgs.makeAutostartItem
+            {
+              name = "goxlr-utility";
+              package = cfg.package;
+            })
+        ];
+    };
+
+  meta.maintainers = with maintainers; [ errnoh ];
+}
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
index 1620770e5b56..6f4cbab81726 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -333,6 +333,8 @@ in
               backup.rcloneConfig);
             path = [ pkgs.openssh ];
             restartIfChanged = false;
+            wants = [ "network-online.target" ];
+            after = [ "network-online.target" ];
             serviceConfig = {
               Type = "oneshot";
               ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup ${concatStringsSep " " (backup.extraBackupArgs ++ excludeFlags)} ${backupPaths}" ])
diff --git a/nixos/modules/services/continuous-integration/github-runner/options.nix b/nixos/modules/services/continuous-integration/github-runner/options.nix
index ce8809213724..f2887c7711b3 100644
--- a/nixos/modules/services/continuous-integration/github-runner/options.nix
+++ b/nixos/modules/services/continuous-integration/github-runner/options.nix
@@ -208,4 +208,12 @@ with lib;
     '';
     default = null;
   };
+
+  nodeRuntimes = mkOption {
+    type = with types; nonEmptyListOf (enum [ "node16" "node20" ]);
+    default = [ "node20" ];
+    description = mdDoc ''
+      List of Node.js runtimes the runner should support.
+    '';
+  };
 }
diff --git a/nixos/modules/services/continuous-integration/github-runner/service.nix b/nixos/modules/services/continuous-integration/github-runner/service.nix
index 55df83362cb6..535df7f68e07 100644
--- a/nixos/modules/services/continuous-integration/github-runner/service.nix
+++ b/nixos/modules/services/continuous-integration/github-runner/service.nix
@@ -22,6 +22,7 @@ with lib;
 
 let
   workDir = if cfg.workDir == null then runtimeDir else cfg.workDir;
+  package = cfg.package.override { inherit (cfg) nodeRuntimes; };
 in
 {
   description = "GitHub Actions runner";
@@ -47,7 +48,7 @@ in
 
   serviceConfig = mkMerge [
     {
-      ExecStart = "${cfg.package}/bin/Runner.Listener run --startuptype service";
+      ExecStart = "${package}/bin/Runner.Listener run --startuptype service";
 
       # Does the following, sequentially:
       # - If the module configuration or the token has changed, purge the state directory,
@@ -149,7 +150,7 @@ in
               else
                 args+=(--token "$token")
               fi
-              ${cfg.package}/bin/Runner.Listener configure "''${args[@]}"
+              ${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/"
diff --git a/nixos/modules/services/databases/influxdb2.nix b/nixos/modules/services/databases/influxdb2.nix
index 329533b35dc8..3740cd01b5dc 100644
--- a/nixos/modules/services/databases/influxdb2.nix
+++ b/nixos/modules/services/databases/influxdb2.nix
@@ -3,34 +3,291 @@
 let
   inherit
     (lib)
+    any
+    attrNames
+    attrValues
+    count
     escapeShellArg
+    filterAttrs
+    flatten
+    flip
+    getExe
     hasAttr
+    hasInfix
+    listToAttrs
     literalExpression
+    mapAttrsToList
+    mdDoc
     mkEnableOption
     mkIf
     mkOption
+    nameValuePair
+    optional
+    subtractLists
     types
+    unique
     ;
 
   format = pkgs.formats.json { };
   cfg = config.services.influxdb2;
   configFile = format.generate "config.json" cfg.settings;
+
+  validPermissions = [
+    "authorizations"
+    "buckets"
+    "dashboards"
+    "orgs"
+    "tasks"
+    "telegrafs"
+    "users"
+    "variables"
+    "secrets"
+    "labels"
+    "views"
+    "documents"
+    "notificationRules"
+    "notificationEndpoints"
+    "checks"
+    "dbrp"
+    "annotations"
+    "sources"
+    "scrapers"
+    "notebooks"
+    "remotes"
+    "replications"
+  ];
+
+  # Determines whether at least one active api token is defined
+  anyAuthDefined =
+    flip any (attrValues cfg.provision.organizations)
+    (o: o.present && flip any (attrValues o.auths)
+    (a: a.present && a.tokenFile != null));
+
+  provisionState = pkgs.writeText "provision_state.json" (builtins.toJSON {
+    inherit (cfg.provision) organizations users;
+  });
+
+  provisioningScript = pkgs.writeShellScript "post-start-provision" ''
+    set -euo pipefail
+    export INFLUX_HOST="http://"${escapeShellArg (
+      if ! hasAttr "http-bind-address" cfg.settings
+        || hasInfix "0.0.0.0" cfg.settings.http-bind-address
+      then "localhost:8086"
+      else cfg.settings.http-bind-address
+    )}
+
+    # Wait for the influxdb server to come online
+    count=0
+    while ! influx ping &>/dev/null; do
+      if [ "$count" -eq 300 ]; then
+        echo "Tried for 30 seconds, giving up..."
+        exit 1
+      fi
+
+      if ! kill -0 "$MAINPID"; then
+        echo "Main server died, giving up..."
+        exit 1
+      fi
+
+      sleep 0.1
+      count=$((count++))
+    done
+
+    # Do the initial database setup. Pass /dev/null as configs-path to
+    # avoid saving the token as the active config.
+    if test -e "$STATE_DIRECTORY/.first_startup"; then
+      influx setup \
+        --configs-path /dev/null \
+        --org ${escapeShellArg cfg.provision.initialSetup.organization} \
+        --bucket ${escapeShellArg cfg.provision.initialSetup.bucket} \
+        --username ${escapeShellArg cfg.provision.initialSetup.username} \
+        --password "$(< "$CREDENTIALS_DIRECTORY/admin-password")" \
+        --token "$(< "$CREDENTIALS_DIRECTORY/admin-token")" \
+        --retention ${toString cfg.provision.initialSetup.retention}s \
+        --force >/dev/null
+
+      rm -f "$STATE_DIRECTORY/.first_startup"
+    fi
+
+    provision_result=$(${getExe pkgs.influxdb2-provision} ${provisionState} "$INFLUX_HOST" "$(< "$CREDENTIALS_DIRECTORY/admin-token")")
+    if [[ "$(jq '[.auths[] | select(.action == "created")] | length' <<< "$provision_result")" -gt 0 ]]; then
+      echo "Created at least one new token, queueing service restart so we can manipulate secrets"
+      touch "$STATE_DIRECTORY/.needs_restart"
+    fi
+  '';
+
+  restarterScript = pkgs.writeShellScript "post-start-restarter" ''
+    set -euo pipefail
+    if test -e "$STATE_DIRECTORY/.needs_restart"; then
+      rm -f "$STATE_DIRECTORY/.needs_restart"
+      /run/current-system/systemd/bin/systemctl restart influxdb2
+    fi
+  '';
+
+  organizationSubmodule = types.submodule (organizationSubmod: let
+    org = organizationSubmod.config._module.args.name;
+  in {
+    options = {
+      present = mkOption {
+        description = mdDoc "Whether to ensure that this organization is present or absent.";
+        type = types.bool;
+        default = true;
+      };
+
+      description = mkOption {
+        description = mdDoc "Optional description for the organization.";
+        default = null;
+        type = types.nullOr types.str;
+      };
+
+      buckets = mkOption {
+        description = mdDoc "Buckets to provision in this organization.";
+        default = {};
+        type = types.attrsOf (types.submodule (bucketSubmod: let
+          bucket = bucketSubmod.config._module.args.name;
+        in {
+          options = {
+            present = mkOption {
+              description = mdDoc "Whether to ensure that this bucket is present or absent.";
+              type = types.bool;
+              default = true;
+            };
+
+            description = mkOption {
+              description = mdDoc "Optional description for the bucket.";
+              default = null;
+              type = types.nullOr types.str;
+            };
+
+            retention = mkOption {
+              type = types.ints.unsigned;
+              default = 0;
+              description = mdDoc "The duration in seconds for which the bucket will retain data (0 is infinite).";
+            };
+          };
+        }));
+      };
+
+      auths = mkOption {
+        description = mdDoc "API tokens to provision for the user in this organization.";
+        default = {};
+        type = types.attrsOf (types.submodule (authSubmod: let
+          auth = authSubmod.config._module.args.name;
+        in {
+          options = {
+            id = mkOption {
+              description = mdDoc "A unique identifier for this authentication token. Since influx doesn't store names for tokens, this will be hashed and appended to the description to identify the token.";
+              readOnly = true;
+              default = builtins.substring 0 32 (builtins.hashString "sha256" "${org}:${auth}");
+              defaultText = "<a hash derived from org and name>";
+              type = types.str;
+            };
+
+            present = mkOption {
+              description = mdDoc "Whether to ensure that this user is present or absent.";
+              type = types.bool;
+              default = true;
+            };
+
+            description = mkOption {
+              description = ''
+                Optional description for the API token.
+                Note that the actual token will always be created with a descriptionregardless
+                of whether this is given or not. The name is always added plus a unique suffix
+                to later identify the token to track whether it has already been created.
+              '';
+              default = null;
+              type = types.nullOr types.str;
+            };
+
+            tokenFile = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = mdDoc "The token value. If not given, influx will automatically generate one.";
+            };
+
+            operator = mkOption {
+              description = mdDoc "Grants all permissions in all organizations.";
+              default = false;
+              type = types.bool;
+            };
+
+            allAccess = mkOption {
+              description = mdDoc "Grants all permissions in the associated organization.";
+              default = false;
+              type = types.bool;
+            };
+
+            readPermissions = mkOption {
+              description = mdDoc ''
+                The read permissions to include for this token. Access is usually granted only
+                for resources in the associated organization.
+
+                Available permissions are `authorizations`, `buckets`, `dashboards`,
+                `orgs`, `tasks`, `telegrafs`, `users`, `variables`, `secrets`, `labels`, `views`,
+                `documents`, `notificationRules`, `notificationEndpoints`, `checks`, `dbrp`,
+                `annotations`, `sources`, `scrapers`, `notebooks`, `remotes`, `replications`.
+
+                Refer to `influx auth create --help` for a full list with descriptions.
+
+                `buckets` grants read access to all associated buckets. Use `readBuckets` to define
+                more granular access permissions.
+              '';
+              default = [];
+              type = types.listOf (types.enum validPermissions);
+            };
+
+            writePermissions = mkOption {
+              description = mdDoc ''
+                The read permissions to include for this token. Access is usually granted only
+                for resources in the associated organization.
+
+                Available permissions are `authorizations`, `buckets`, `dashboards`,
+                `orgs`, `tasks`, `telegrafs`, `users`, `variables`, `secrets`, `labels`, `views`,
+                `documents`, `notificationRules`, `notificationEndpoints`, `checks`, `dbrp`,
+                `annotations`, `sources`, `scrapers`, `notebooks`, `remotes`, `replications`.
+
+                Refer to `influx auth create --help` for a full list with descriptions.
+
+                `buckets` grants write access to all associated buckets. Use `writeBuckets` to define
+                more granular access permissions.
+              '';
+              default = [];
+              type = types.listOf (types.enum validPermissions);
+            };
+
+            readBuckets = mkOption {
+              description = mdDoc "The organization's buckets which should be allowed to be read";
+              default = [];
+              type = types.listOf types.str;
+            };
+
+            writeBuckets = mkOption {
+              description = mdDoc "The organization's buckets which should be allowed to be written";
+              default = [];
+              type = types.listOf types.str;
+            };
+          };
+        }));
+      };
+    };
+  });
 in
 {
   options = {
     services.influxdb2 = {
-      enable = mkEnableOption (lib.mdDoc "the influxdb2 server");
+      enable = mkEnableOption (mdDoc "the influxdb2 server");
 
       package = mkOption {
         default = pkgs.influxdb2-server;
         defaultText = literalExpression "pkgs.influxdb2";
-        description = lib.mdDoc "influxdb2 derivation to use.";
+        description = mdDoc "influxdb2 derivation to use.";
         type = types.package;
       };
 
       settings = mkOption {
         default = { };
-        description = lib.mdDoc ''configuration options for influxdb2, see <https://docs.influxdata.com/influxdb/v2.0/reference/config-options> for details.'';
+        description = mdDoc ''configuration options for influxdb2, see <https://docs.influxdata.com/influxdb/v2.0/reference/config-options> for details.'';
         type = format.type;
       };
 
@@ -41,52 +298,135 @@ in
           organization = mkOption {
             type = types.str;
             example = "main";
-            description = "Primary organization name";
+            description = mdDoc "Primary organization name";
           };
 
           bucket = mkOption {
             type = types.str;
             example = "example";
-            description = "Primary bucket name";
+            description = mdDoc "Primary bucket name";
           };
 
           username = mkOption {
             type = types.str;
             default = "admin";
-            description = "Primary username";
+            description = mdDoc "Primary username";
           };
 
           retention = mkOption {
-            type = types.str;
-            default = "0";
-            description = ''
-              The duration for which the bucket will retain data (0 is infinite).
-              Accepted units are `ns` (nanoseconds), `us` or `µs` (microseconds), `ms` (milliseconds),
-              `s` (seconds), `m` (minutes), `h` (hours), `d` (days) and `w` (weeks).
-            '';
+            type = types.ints.unsigned;
+            default = 0;
+            description = mdDoc "The duration in seconds for which the bucket will retain data (0 is infinite).";
           };
 
           passwordFile = mkOption {
             type = types.path;
-            description = "Password for primary user. Don't use a file from the nix store!";
+            description = mdDoc "Password for primary user. Don't use a file from the nix store!";
           };
 
           tokenFile = mkOption {
             type = types.path;
-            description = "API Token to set for the admin user. Don't use a file from the nix store!";
+            description = mdDoc "API Token to set for the admin user. Don't use a file from the nix store!";
           };
         };
+
+        organizations = mkOption {
+          description = mdDoc "Organizations to provision.";
+          example = literalExpression ''
+            {
+              myorg = {
+                description = "My organization";
+                buckets.mybucket = {
+                  description = "My bucket";
+                  retention = 31536000; # 1 year
+                };
+                auths.mytoken = {
+                  readBuckets = ["mybucket"];
+                  tokenFile = "/run/secrets/mytoken";
+                };
+              };
+            }
+          '';
+          default = {};
+          type = types.attrsOf organizationSubmodule;
+        };
+
+        users = mkOption {
+          description = mdDoc "Users to provision.";
+          default = {};
+          example = literalExpression ''
+            {
+              # admin = {}; /* The initialSetup.username will automatically be added. */
+              myuser.passwordFile = "/run/secrets/myuser_password";
+            }
+          '';
+          type = types.attrsOf (types.submodule (userSubmod: let
+            user = userSubmod.config._module.args.name;
+            org = userSubmod.config.org;
+          in {
+            options = {
+              present = mkOption {
+                description = mdDoc "Whether to ensure that this user is present or absent.";
+                type = types.bool;
+                default = true;
+              };
+
+              passwordFile = mkOption {
+                description = mdDoc "Password for the user. If unset, the user will not be able to log in until a password is set by an operator! Don't use a file from the nix store!";
+                default = null;
+                type = types.nullOr types.path;
+              };
+            };
+          }));
+        };
       };
     };
   };
 
   config = mkIf cfg.enable {
-    assertions = [
-      {
-        assertion = !(hasAttr "bolt-path" cfg.settings) && !(hasAttr "engine-path" cfg.settings);
-        message = "services.influxdb2.config: bolt-path and engine-path should not be set as they are managed by systemd";
-      }
-    ];
+    assertions =
+      [
+        {
+          assertion = !(hasAttr "bolt-path" cfg.settings) && !(hasAttr "engine-path" cfg.settings);
+          message = "services.influxdb2.config: bolt-path and engine-path should not be set as they are managed by systemd";
+        }
+      ]
+      ++ flatten (flip mapAttrsToList cfg.provision.organizations (orgName: org:
+        flip mapAttrsToList org.auths (authName: auth:
+          [
+            {
+              assertion = 1 == count (x: x) [
+                auth.operator
+                auth.allAccess
+                (auth.readPermissions != []
+                  || auth.writePermissions != []
+                  || auth.readBuckets != []
+                  || auth.writeBuckets != [])
+              ];
+              message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: The `operator` and `allAccess` options are mutually exclusive with each other and the granular permission settings.";
+            }
+            (let unknownBuckets = subtractLists (attrNames org.buckets) auth.readBuckets; in {
+              assertion = unknownBuckets == [];
+              message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: Refers to invalid buckets in readBuckets: ${toString unknownBuckets}";
+            })
+            (let unknownBuckets = subtractLists (attrNames org.buckets) auth.writeBuckets; in {
+              assertion = unknownBuckets == [];
+              message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: Refers to invalid buckets in writeBuckets: ${toString unknownBuckets}";
+            })
+          ]
+        )
+      ));
+
+    services.influxdb2.provision = mkIf cfg.provision.enable {
+      organizations.${cfg.provision.initialSetup.organization} = {
+        buckets.${cfg.provision.initialSetup.bucket} = {
+          inherit (cfg.provision.initialSetup) retention;
+        };
+      };
+      users.${cfg.provision.initialSetup.username} = {
+        inherit (cfg.provision.initialSetup) passwordFile;
+      };
+    };
 
     systemd.services.influxdb2 = {
       description = "InfluxDB is an open-source, distributed, time series database";
@@ -111,58 +451,38 @@ in
           "admin-password:${cfg.provision.initialSetup.passwordFile}"
           "admin-token:${cfg.provision.initialSetup.tokenFile}"
         ];
+
+        ExecStartPost = mkIf cfg.provision.enable (
+          [provisioningScript] ++
+          # Only the restarter runs with elevated privileges
+          optional anyAuthDefined "+${restarterScript}"
+        );
       };
 
-      path = [pkgs.influxdb2-cli];
+      path = [
+        pkgs.influxdb2-cli
+        pkgs.jq
+      ];
 
-      # Mark if this is the first startup so postStart can do the initial setup
-      preStart = mkIf cfg.provision.enable ''
+      # Mark if this is the first startup so postStart can do the initial setup.
+      # Also extract any token secret mappings and apply them if this isn't the first start.
+      preStart = let
+        tokenPaths = listToAttrs (flatten
+          # For all organizations
+          (flip mapAttrsToList cfg.provision.organizations
+            # For each contained token that has a token file
+            (_: org: flip mapAttrsToList (filterAttrs (_: x: x.tokenFile != null) org.auths)
+              # Collect id -> tokenFile for the mapping
+              (_: auth: nameValuePair auth.id auth.tokenFile))));
+        tokenMappings = pkgs.writeText "token_mappings.json" (builtins.toJSON tokenPaths);
+      in mkIf cfg.provision.enable ''
         if ! test -e "$STATE_DIRECTORY/influxd.bolt"; then
           touch "$STATE_DIRECTORY/.first_startup"
+        else
+          # Manipulate provisioned api tokens if necessary
+          ${getExe pkgs.influxdb2-token-manipulator} "$STATE_DIRECTORY/influxd.bolt" ${tokenMappings}
         fi
       '';
-
-      postStart = let
-        initCfg = cfg.provision.initialSetup;
-      in mkIf cfg.provision.enable (
-        ''
-          set -euo pipefail
-          export INFLUX_HOST="http://"${escapeShellArg (cfg.settings.http-bind-address or "localhost:8086")}
-
-          # Wait for the influxdb server to come online
-          count=0
-          while ! influx ping &>/dev/null; do
-            if [ "$count" -eq 300 ]; then
-              echo "Tried for 30 seconds, giving up..."
-              exit 1
-            fi
-
-            if ! kill -0 "$MAINPID"; then
-              echo "Main server died, giving up..."
-              exit 1
-            fi
-
-            sleep 0.1
-            count=$((count++))
-          done
-
-          # Do the initial database setup. Pass /dev/null as configs-path to
-          # avoid saving the token as the active config.
-          if test -e "$STATE_DIRECTORY/.first_startup"; then
-            influx setup \
-              --configs-path /dev/null \
-              --org ${escapeShellArg initCfg.organization} \
-              --bucket ${escapeShellArg initCfg.bucket} \
-              --username ${escapeShellArg initCfg.username} \
-              --password "$(< "$CREDENTIALS_DIRECTORY/admin-password")" \
-              --token "$(< "$CREDENTIALS_DIRECTORY/admin-token")" \
-              --retention ${escapeShellArg initCfg.retention} \
-              --force >/dev/null
-
-            rm -f "$STATE_DIRECTORY/.first_startup"
-          fi
-        ''
-      );
     };
 
     users.extraUsers.influxdb2 = {
diff --git a/nixos/modules/services/editors/emacs.md b/nixos/modules/services/editors/emacs.md
index 72364b295144..9db1bd594175 100644
--- a/nixos/modules/services/editors/emacs.md
+++ b/nixos/modules/services/editors/emacs.md
@@ -286,11 +286,11 @@ 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
+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:
+To connect to the Emacs daemon, run one of the following:
 ```
 emacsclient FILENAME
 emacsclient --create-frame  # opens a new frame (window)
@@ -339,24 +339,10 @@ This will add the symlink
 
 ## Configuring Emacs {#module-services-emacs-configuring}
 
-The Emacs init file should be changed to load the extension packages at
-startup:
+If you want to only use extension packages from Nixpkgs, you can add
+`(setq package-archives nil)` to your init file.
 
-::: {.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,
+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).
diff --git a/nixos/modules/services/editors/emacs.nix b/nixos/modules/services/editors/emacs.nix
index fe3a10159794..fad4f39ff210 100644
--- a/nixos/modules/services/editors/emacs.nix
+++ b/nixos/modules/services/editors/emacs.nix
@@ -80,6 +80,15 @@ in
         using the EDITOR environment variable.
       '';
     };
+
+    startWithGraphical = mkOption {
+      type = types.bool;
+      default = config.services.xserver.enable;
+      defaultText = literalExpression "config.services.xserver.enable";
+      description = lib.mdDoc ''
+        Start emacs with the graphical session instead of any session. Without this, emacs clients will not be able to create frames in the graphical session.
+      '';
+    };
   };
 
   config = mkIf (cfg.enable || cfg.install) {
@@ -92,7 +101,13 @@ in
         ExecStop = "${cfg.package}/bin/emacsclient --eval (kill-emacs)";
         Restart = "always";
       };
-    } // optionalAttrs cfg.enable { wantedBy = [ "default.target" ]; };
+
+      unitConfig = optionalAttrs cfg.startWithGraphical {
+        After = "graphical-session.target";
+      };
+    } // optionalAttrs cfg.enable {
+      wantedBy = if cfg.startWithGraphical then [ "graphical-session.target" ] else [ "default.target" ];
+    };
 
     environment.systemPackages = [ cfg.package editorScript desktopApplicationFile ];
 
diff --git a/nixos/modules/services/finance/odoo.nix b/nixos/modules/services/finance/odoo.nix
index fee9af574b5d..eec7c4e30cc4 100644
--- a/nixos/modules/services/finance/odoo.nix
+++ b/nixos/modules/services/finance/odoo.nix
@@ -31,6 +31,12 @@ in
         description = lib.mdDoc ''
           Odoo configuration settings. For more details see <https://www.odoo.com/documentation/15.0/administration/install/deploy.html>
         '';
+        example = literalExpression ''
+          options = {
+            db_user = "odoo";
+            db_password="odoo";
+          };
+        '';
       };
 
       domain = mkOption {
@@ -112,11 +118,11 @@ in
     services.postgresql = {
       enable = true;
 
+      ensureDatabases = [ "odoo" ];
       ensureUsers = [{
         name = "odoo";
         ensurePermissions = { "DATABASE odoo" = "ALL PRIVILEGES"; };
       }];
-      ensureDatabases = [ "odoo" ];
     };
   });
 }
diff --git a/nixos/modules/services/hardware/auto-cpufreq.nix b/nixos/modules/services/hardware/auto-cpufreq.nix
index fd2e03ef12f5..cf27bdd8b6eb 100644
--- a/nixos/modules/services/hardware/auto-cpufreq.nix
+++ b/nixos/modules/services/hardware/auto-cpufreq.nix
@@ -15,8 +15,7 @@ in {
         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
+          The available options can be found in [the example configuration file](https://github.com/AdnanHodzic/auto-cpufreq/blob/v${pkgs.auto-cpufreq.version}/auto-cpufreq.conf-example).
           '';
 
         default = {};
@@ -35,6 +34,7 @@ in {
         wantedBy = [ "multi-user.target" ];
         path = with pkgs; [ bash coreutils ];
 
+        serviceConfig.WorkingDirectory = "";
         serviceConfig.ExecStart = [
           ""
           "${lib.getExe pkgs.auto-cpufreq} --daemon --config ${cfgFile}"
@@ -42,4 +42,7 @@ in {
       };
     };
   };
+
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
 }
diff --git a/nixos/modules/services/hardware/hddfancontrol.nix b/nixos/modules/services/hardware/hddfancontrol.nix
new file mode 100644
index 000000000000..f472b5774cbf
--- /dev/null
+++ b/nixos/modules/services/hardware/hddfancontrol.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.hddfancontrol;
+  types = lib.types;
+in
+
+{
+  options = {
+
+    services.hddfancontrol.enable = lib.mkEnableOption (lib.mdDoc "hddfancontrol daemon");
+
+    services.hddfancontrol.disks = lib.mkOption {
+      type = with types; listOf path;
+      default = [];
+      description = lib.mdDoc ''
+        Drive(s) to get temperature from
+      '';
+      example = ["/dev/sda"];
+    };
+
+    services.hddfancontrol.pwmPaths = lib.mkOption {
+      type = with types; listOf path;
+      default = [];
+      description = lib.mdDoc ''
+        PWM filepath(s) to control fan speed (under /sys)
+      '';
+      example = ["/sys/class/hwmon/hwmon2/pwm1"];
+    };
+
+    services.hddfancontrol.smartctl = lib.mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Probe temperature using smartctl instead of hddtemp or hdparm
+      '';
+    };
+
+    services.hddfancontrol.extraArgs = lib.mkOption {
+      type = with types; listOf str;
+      default = [];
+      description = lib.mdDoc ''
+        Extra commandline arguments for hddfancontrol
+      '';
+      example = ["--pwm-start-value=32"
+                 "--pwm-stop-value=0"
+                 "--spin-down-time=900"];
+    };
+  };
+
+  config = lib.mkIf cfg.enable (
+    let args = lib.concatLists [
+      ["-d"] cfg.disks
+      ["-p"] cfg.pwmPaths
+      (lib.optional cfg.smartctl "--smartctl")
+      cfg.extraArgs
+    ]; in {
+      systemd.packages = [pkgs.hddfancontrol];
+
+      systemd.services.hddfancontrol = {
+        wantedBy = [ "multi-user.target" ];
+        environment.HDDFANCONTROL_ARGS = lib.escapeShellArgs args;
+      };
+    }
+  );
+}
diff --git a/nixos/modules/services/logging/graylog.nix b/nixos/modules/services/logging/graylog.nix
index 1eb51c50ff79..673930c4cb5c 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 = 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");
+        default = if versionOlder config.system.stateVersion "23.05" then pkgs.graylog-3_3 else pkgs.graylog-5_1;
+        defaultText = literalExpression (if versionOlder config.system.stateVersion "23.05" then "pkgs.graylog-3_3" else "pkgs.graylog-5_1");
         description = lib.mdDoc "Graylog package to use.";
       };
 
diff --git a/nixos/modules/services/mail/stalwart-mail.nix b/nixos/modules/services/mail/stalwart-mail.nix
new file mode 100644
index 000000000000..fdbdc99070b9
--- /dev/null
+++ b/nixos/modules/services/mail/stalwart-mail.nix
@@ -0,0 +1,106 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.stalwart-mail;
+  configFormat = pkgs.formats.toml { };
+  configFile = configFormat.generate "stalwart-mail.toml" cfg.settings;
+  dataDir = "/var/lib/stalwart-mail";
+
+in {
+  options.services.stalwart-mail = {
+    enable = mkEnableOption (mdDoc "the Stalwart all-in-one email server");
+    package = mkPackageOptionMD pkgs "stalwart-mail" { };
+
+    settings = mkOption {
+      inherit (configFormat) type;
+      default = { };
+      description = mdDoc ''
+        Configuration options for the Stalwart email server.
+        See <https://stalw.art/docs/> for available options.
+
+        By default, the module is configured to store everything locally.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Default config: all local
+    services.stalwart-mail.settings = {
+      global.tracing.method = mkDefault "stdout";
+      global.tracing.level = mkDefault "info";
+      queue.path = mkDefault "${dataDir}/queue";
+      report.path = mkDefault "${dataDir}/reports";
+      store.db.path = mkDefault "${dataDir}/data/index.sqlite3";
+      store.blob.type = mkDefault "local";
+      store.blob.local.path = mkDefault "${dataDir}/data/blobs";
+      resolver.type = mkDefault "system";
+    };
+
+    systemd.services.stalwart-mail = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "local-fs.target" "network.target" ];
+
+      preStart = ''
+        mkdir -p ${dataDir}/{queue,reports,data/blobs}
+      '';
+
+      serviceConfig = {
+        ExecStart =
+          "${cfg.package}/bin/stalwart-mail --config=${configFile}";
+
+        # Base from template resources/systemd/stalwart-mail.service
+        Type = "simple";
+        LimitNOFILE = 65536;
+        KillMode = "process";
+        KillSignal = "SIGINT";
+        Restart = "on-failure";
+        RestartSec = 5;
+        StandardOutput = "syslog";
+        StandardError = "syslog";
+        SyslogIdentifier = "stalwart-mail";
+
+        DynamicUser = true;
+        User = "stalwart-mail";
+        StateDirectory = "stalwart-mail";
+
+        # Bind standard privileged ports
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+
+        # Hardening
+        DeviceAllow = [ "" ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        PrivateDevices = true;
+        PrivateUsers = false;  # incompatible with CAP_NET_BIND_SERVICE
+        ProcSubset = "pid";
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
+        UMask = "0077";
+      };
+    };
+
+    # Make admin commands available in the shell
+    environment.systemPackages = [ cfg.package ];
+  };
+
+  meta = {
+    maintainers = with maintainers; [ happysalada pacien ];
+  };
+}
diff --git a/nixos/modules/services/matrix/mautrix-telegram.nix b/nixos/modules/services/matrix/mautrix-telegram.nix
index 17032ed808e9..97a6ba858e00 100644
--- a/nixos/modules/services/matrix/mautrix-telegram.nix
+++ b/nixos/modules/services/matrix/mautrix-telegram.nix
@@ -159,7 +159,6 @@ in {
         if [ ! -f '${registrationFile}' ]; then
           ${pkgs.mautrix-telegram}/bin/mautrix-telegram \
             --generate-registration \
-            --base-config='${pkgs.mautrix-telegram}/${pkgs.mautrix-telegram.pythonModule.sitePackages}/mautrix_telegram/example-config.yaml' \
             --config='${settingsFile}' \
             --registration='${registrationFile}'
         fi
diff --git a/nixos/modules/services/matrix/mautrix-whatsapp.nix b/nixos/modules/services/matrix/mautrix-whatsapp.nix
index 80c85980196f..c4dc48213495 100644
--- a/nixos/modules/services/matrix/mautrix-whatsapp.nix
+++ b/nixos/modules/services/matrix/mautrix-whatsapp.nix
@@ -11,53 +11,47 @@
   settingsFileUnsubstituted = settingsFormat.generate "mautrix-whatsapp-config-unsubstituted.json" cfg.settings;
   settingsFormat = pkgs.formats.json {};
   appservicePort = 29318;
+
+  mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v);
+  defaultConfig = {
+    homeserver.address = "http://localhost:8448";
+    appservice = {
+      hostname = "[::]";
+      port = appservicePort;
+      database.type = "sqlite3";
+      database.uri = "${dataDir}/mautrix-whatsapp.db";
+      id = "whatsapp";
+      bot.username = "whatsappbot";
+      bot.displayname = "WhatsApp Bridge Bot";
+      as_token = "";
+      hs_token = "";
+    };
+    bridge = {
+      username_template = "whatsapp_{{.}}";
+      displayname_template = "{{if .BusinessName}}{{.BusinessName}}{{else if .PushName}}{{.PushName}}{{else}}{{.JID}}{{end}} (WA)";
+      double_puppet_server_map = {};
+      login_shared_secret_map = {};
+      command_prefix = "!wa";
+      permissions."*" = "relay";
+      relay.enabled = true;
+    };
+    logging = {
+      min_level = "info";
+      writers = lib.singleton {
+        type = "stdout";
+        format = "pretty-colored";
+        time_format = " ";
+      };
+    };
+  };
+
 in {
-  imports = [];
   options.services.mautrix-whatsapp = {
-    enable = lib.mkEnableOption "mautrix-whatsapp, a puppeting/relaybot bridge between Matrix and WhatsApp.";
+    enable = lib.mkEnableOption (lib.mdDoc "mautrix-whatsapp, a puppeting/relaybot bridge between Matrix and WhatsApp.");
 
     settings = lib.mkOption {
       type = settingsFormat.type;
-      default = {
-        appservice = {
-          address = "http://localhost:${toString appservicePort}";
-          hostname = "[::]";
-          port = appservicePort;
-          database = {
-            type = "sqlite3";
-            uri = "${dataDir}/mautrix-whatsapp.db";
-          };
-          id = "whatsapp";
-          bot = {
-            username = "whatsappbot";
-            displayname = "WhatsApp Bridge Bot";
-          };
-          as_token = "";
-          hs_token = "";
-        };
-        bridge = {
-          username_template = "whatsapp_{{.}}";
-          displayname_template = "{{if .BusinessName}}{{.BusinessName}}{{else if .PushName}}{{.PushName}}{{else}}{{.JID}}{{end}} (WA)";
-          double_puppet_server_map = {};
-          login_shared_secret_map = {};
-          command_prefix = "!wa";
-          permissions."*" = "relay";
-          relay.enabled = true;
-        };
-        logging = {
-          min_level = "info";
-          writers = [
-            {
-              type = "stdout";
-              format = "pretty-colored";
-            }
-            {
-              type = "file";
-              format = "json";
-            }
-          ];
-        };
-      };
+      default = defaultConfig;
       description = lib.mdDoc ''
         {file}`config.yaml` configuration as a Nix attribute set.
         Configuration options should match those described in
@@ -117,10 +111,22 @@ in {
   };
 
   config = lib.mkIf cfg.enable {
-    services.mautrix-whatsapp.settings = {
-      homeserver.domain = lib.mkDefault config.services.matrix-synapse.settings.server_name;
+
+    users.users.mautrix-whatsapp = {
+      isSystemUser = true;
+      group = "mautrix-whatsapp";
+      home = dataDir;
+      description = "Mautrix-WhatsApp bridge user";
     };
 
+    users.groups.mautrix-whatsapp = {};
+
+    services.mautrix-whatsapp.settings = lib.mkMerge (map mkDefaults [
+      defaultConfig
+      # Note: this is defined here to avoid the docs depending on `config`
+      { homeserver.domain = config.services.matrix-synapse.settings.server_name; }
+    ]);
+
     systemd.services.mautrix-whatsapp = {
       description = "Mautrix-WhatsApp Service - A WhatsApp bridge for Matrix";
 
@@ -158,10 +164,11 @@ in {
       '';
 
       serviceConfig = {
-        DynamicUser = true;
+        User = "mautrix-whatsapp";
+        Group = "mautrix-whatsapp";
         EnvironmentFile = cfg.environmentFile;
         StateDirectory = baseNameOf dataDir;
-        WorkingDirectory = "${dataDir}";
+        WorkingDirectory = dataDir;
         ExecStart = ''
           ${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp \
           --config='${settingsFile}' \
diff --git a/nixos/modules/services/misc/atuin.nix b/nixos/modules/services/misc/atuin.nix
index 57ff02df7d68..8d2c1b5242ff 100644
--- a/nixos/modules/services/misc/atuin.nix
+++ b/nixos/modules/services/misc/atuin.nix
@@ -6,7 +6,7 @@ in
 {
   options = {
     services.atuin = {
-      enable = lib.mkEnableOption (mdDoc "Enable server for shell history sync with atuin");
+      enable = lib.mkEnableOption (mdDoc "Atuin server for shell history sync");
 
       openRegistration = mkOption {
         type = types.bool;
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index c5e38b498829..b399ccc38f58 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -1088,6 +1088,11 @@ in {
         ''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.''
       )
+      (mkIf
+        (versionAtLeast (getVersion cfg.packages.gitlab) "16.2.0" && versionOlder (getVersion cfg.packages.gitlab) "16.5.0")
+        ''GitLab instances created or updated between versions [15.11.0, 15.11.2] have an incorrect database schema.
+        Check the upstream documentation for a workaround: https://docs.gitlab.com/ee/update/versions/gitlab_16_changes.html#undefined-column-error-upgrading-to-162-or-later''
+      )
     ];
 
     assertions = [
@@ -1655,7 +1660,7 @@ in {
         Restart = "on-failure";
         WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
         ExecStart = concatStringsSep " " [
-          "${cfg.packages.gitlab.rubyEnv}/bin/puma"
+          "${cfg.packages.gitlab.rubyEnv}/bin/bundle" "exec" "puma"
           "-e production"
           "-C ${cfg.statePath}/config/puma.rb"
           "-w ${cfg.puma.workers}"
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index 0683a1f922ab..74a3b49ac9a6 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -43,6 +43,8 @@ let
       "-/etc/nsswitch.conf"
       "-/etc/hosts"
       "-/etc/localtime"
+      "-/etc/ssl/certs"
+      "-/etc/static/ssl/certs"
       "-/run/postgresql"
     ] ++ (optional enableRedis redisServer.unixSocket);
     BindPaths = [
diff --git a/nixos/modules/services/monitoring/mimir.nix b/nixos/modules/services/monitoring/mimir.nix
index edca9b7be4ff..6ed139b22974 100644
--- a/nixos/modules/services/monitoring/mimir.nix
+++ b/nixos/modules/services/monitoring/mimir.nix
@@ -32,11 +32,21 @@ in {
       type = types.package;
       description = lib.mdDoc ''Mimir package to use.'';
     };
+
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "--config.expand-env=true" ];
+      description = lib.mdDoc ''
+        Specify a list of additional command line flags,
+        which get escaped and are then passed to Mimir.
+      '';
+    };
   };
 
   config = mkIf cfg.enable {
     # for mimirtool
-    environment.systemPackages = [ pkgs.mimir ];
+    environment.systemPackages = [ cfg.package ];
 
     assertions = [{
       assertion = (
@@ -60,7 +70,7 @@ in {
                else cfg.configFile;
       in
       {
-        ExecStart = "${cfg.package}/bin/mimir --config.file=${conf}";
+        ExecStart = "${cfg.package}/bin/mimir --config.file=${conf} ${escapeShellArgs cfg.extraFlags}";
         DynamicUser = true;
         Restart = "always";
         ProtectSystem = "full";
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index f5b97c51186a..8bb017894ee2 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -50,6 +50,7 @@ let
     "mikrotik"
     "minio"
     "modemmanager"
+    "mysqld"
     "nextcloud"
     "nginx"
     "nginxlog"
@@ -297,6 +298,12 @@ in
           or 'services.prometheus.exporters.mail.configFile'.
       '';
     } {
+      assertion = cfg.mysqld.runAsLocalSuperUser -> config.services.mysql.enable;
+      message = ''
+        The exporter is configured to run as 'services.mysql.user', but
+          'services.mysql.enable' is set to false.
+      '';
+    } {
       assertion = cfg.sql.enable -> (
         (cfg.sql.configFile == null) != (cfg.sql.configuration == null)
       );
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/mysqld.nix b/nixos/modules/services/monitoring/prometheus/exporters/mysqld.nix
new file mode 100644
index 000000000000..849c514de681
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/mysqld.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, options }:
+let
+  cfg = config.services.prometheus.exporters.mysqld;
+  inherit (lib) types mkOption mdDoc mkIf mkForce cli concatStringsSep optionalString escapeShellArgs;
+in {
+  port = 9104;
+  extraOpts = {
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = mdDoc ''
+        Path under which to expose metrics.
+      '';
+    };
+
+    runAsLocalSuperUser = mkOption {
+      type = types.bool;
+      default = false;
+      description = mdDoc ''
+        Whether to run the exporter as {option}`services.mysql.user`.
+      '';
+    };
+
+    configFile = mkOption {
+      type = types.path;
+      example = "/var/lib/prometheus-mysqld-exporter.cnf";
+      description = mdDoc ''
+        Path to the services config file.
+
+        See <https://github.com/prometheus/mysqld_exporter#running> for more information about
+        the available options.
+
+        ::: {.warn}
+        Please do not store this file in the nix store if you choose to include any credentials here,
+        as it would be world-readable.
+        :::
+      '';
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = !cfg.runAsLocalSuperUser;
+      User = mkIf cfg.runAsLocalSuperUser (mkForce config.services.mysql.user);
+      LoadCredential = mkIf (cfg.configFile != null) (mkForce ("config:" + cfg.configFile));
+      ExecStart = concatStringsSep " " [
+        "${pkgs.prometheus-mysqld-exporter}/bin/mysqld_exporter"
+        "--web.listen-address=${cfg.listenAddress}:${toString cfg.port}"
+        "--web.telemetry-path=${cfg.telemetryPath}"
+        (optionalString (cfg.configFile != null) ''--config.my-cnf=''${CREDENTIALS_DIRECTORY}/config'')
+        (escapeShellArgs cfg.extraFlags)
+      ];
+      RestrictAddressFamilies = [
+        # The exporter can be configured to talk to a local mysql server via a unix socket.
+        "AF_UNIX"
+      ];
+    };
+  };
+}
+
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix b/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
index 7808c8861a76..28add020f5cc 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
@@ -33,6 +33,15 @@ in
         Make sure that this file is readable by the exporter user.
       '';
     };
+    tokenFile = mkOption {
+      type = types.path;
+      example = "/path/to/token-file";
+      default = "";
+      description = lib.mdDoc ''
+        File containing the token for connecting to Nextcloud.
+        Make sure that this file is readable by the exporter user.
+      '';
+    };
     timeout = mkOption {
       type = types.str;
       default = "5s";
@@ -47,12 +56,14 @@ in
       ExecStart = ''
         ${pkgs.prometheus-nextcloud-exporter}/bin/nextcloud-exporter \
           --addr ${cfg.listenAddress}:${toString cfg.port} \
-          --username ${cfg.username} \
           --timeout ${cfg.timeout} \
           --server ${cfg.url} \
-          --password ${escapeShellArg "@${cfg.passwordFile}"} \
-          ${concatStringsSep " \\\n  " cfg.extraFlags}
-      '';
+          ${if cfg.tokenFile == "" then ''
+            --username ${cfg.username} \
+            --password ${escapeShellArg "@${cfg.passwordFile}"} \
+         '' else ''
+            --auth-token ${escapeShellArg "@${cfg.tokenFile}"} \
+         ''} ${concatStringsSep " \\\n  " cfg.extraFlags}'';
     };
   };
 }
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix b/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix
index f52d92a73d5d..f2336429d42f 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix
@@ -1,4 +1,8 @@
-{ config, lib, pkgs, options }:
+{ config
+, lib
+, pkgs
+, options
+}:
 
 with lib;
 
@@ -6,17 +10,14 @@ let
   cfg = config.services.prometheus.exporters.unbound;
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "controlInterface" ] "This option was removed, use the `unbound.host` option instead.")
+    (mkRemovedOptionModule [ "fetchType" ] "This option was removed, use the `unbound.host` option instead.")
+    ({ options.warnings = options.warnings; options.assertions = options.assertions; })
+  ];
+
   port = 9167;
   extraOpts = {
-    fetchType = mkOption {
-      # TODO: add shm when upstream implemented it
-      type = types.enum [ "tcp" "uds" ];
-      default = "uds";
-      description = lib.mdDoc ''
-        Which methods the exporter uses to get the information from unbound.
-      '';
-    };
-
     telemetryPath = mkOption {
       type = types.str;
       default = "/metrics";
@@ -25,34 +26,65 @@ in
       '';
     };
 
-    controlInterface = mkOption {
-      type = types.nullOr types.str;
-      default = null;
-      example = "/run/unbound/unbound.socket";
-      description = lib.mdDoc ''
-        Path to the unbound socket for uds mode or the control interface port for tcp mode.
+    unbound = {
+      ca = mkOption {
+        type = types.nullOr types.path;
+        default = "/var/lib/unbound/unbound_server.pem";
+        example = null;
+        description = ''
+          Path to the Unbound server certificate authority
+        '';
+      };
 
-        Example:
-          uds-mode: /run/unbound/unbound.socket
-          tcp-mode: 127.0.0.1:8953
-      '';
+      certificate = mkOption {
+        type = types.nullOr types.path;
+        default = "/var/lib/unbound/unbound_control.pem";
+        example = null;
+        description = ''
+          Path to the Unbound control socket certificate
+        '';
+      };
+
+      key = mkOption {
+        type = types.nullOr types.path;
+        default = "/var/lib/unbound/unbound_control.key";
+        example = null;
+        description = ''
+          Path to the Unbound control socket key.
+        '';
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = "tcp://127.0.0.1:8953";
+        example = "unix:///run/unbound/unbound.socket";
+        description = lib.mdDoc ''
+          Path to the unbound control socket. Supports unix domain sockets, as well as the TCP interface.
+        '';
+      };
     };
   };
 
   serviceOpts = mkMerge ([{
     serviceConfig = {
+      User = "unbound"; # to access the unbound_control.key
       ExecStart = ''
-        ${pkgs.prometheus-unbound-exporter}/bin/unbound-telemetry \
-          ${cfg.fetchType} \
-          --bind ${cfg.listenAddress}:${toString cfg.port} \
-          --path ${cfg.telemetryPath} \
-          ${optionalString (cfg.controlInterface != null) "--control-interface ${cfg.controlInterface}"} \
+        ${pkgs.prometheus-unbound-exporter}/bin/unbound_exporter \
+          --unbound.host "${cfg.unbound.host}" \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path ${cfg.telemetryPath} \
+          ${optionalString (cfg.unbound.ca != null) "--unbound.ca ${cfg.unbound.ca}"} \
+          ${optionalString (cfg.unbound.certificate != null) "--unbound.cert ${cfg.unbound.certificate}"} \
+          ${optionalString (cfg.unbound.key != null) "--unbound.key ${cfg.unbound.key}"} \
           ${toString cfg.extraFlags}
       '';
       RestrictAddressFamilies = [
-        # Need AF_UNIX to collect data
         "AF_UNIX"
+        "AF_INET"
+        "AF_INET6"
       ];
+    } // optionalAttrs (!config.services.unbound.enable) {
+      DynamicUser = true;
     };
   }] ++ [
     (mkIf config.services.unbound.enable {
diff --git a/nixos/modules/services/monitoring/vmagent.nix b/nixos/modules/services/monitoring/vmagent.nix
index c793bb073199..0e2ffb31c57c 100644
--- a/nixos/modules/services/monitoring/vmagent.nix
+++ b/nixos/modules/services/monitoring/vmagent.nix
@@ -62,6 +62,16 @@ in {
         Whether to open the firewall for the default ports.
       '';
     };
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = lib.mdDoc ''
+        Extra args to pass to `vmagent`. See the docs:
+        <https://docs.victoriametrics.com/vmagent.html#advanced-usage>
+        or {command}`vmagent -help` for more information.
+      '';
+    };
   };
 
   config = mkIf cfg.enable {
@@ -90,7 +100,7 @@ in {
         Type = "simple";
         Restart = "on-failure";
         WorkingDirectory = cfg.dataDir;
-        ExecStart = "${cfg.package}/bin/vmagent -remoteWrite.url=${cfg.remoteWriteUrl} -promscrape.config=${prometheusConfig}";
+        ExecStart = "${cfg.package}/bin/vmagent -remoteWrite.url=${cfg.remoteWriteUrl} -promscrape.config=${prometheusConfig} ${escapeShellArgs cfg.extraArgs}";
       };
     };
 
diff --git a/nixos/modules/services/networking/dae.nix b/nixos/modules/services/networking/dae.nix
index 231c555b3303..42ed3c7f8d4a 100644
--- a/nixos/modules/services/networking/dae.nix
+++ b/nixos/modules/services/networking/dae.nix
@@ -1,41 +1,161 @@
-{ config, pkgs, lib, ... }:
+{ config, lib, pkgs, ... }:
+
 let
   cfg = config.services.dae;
+  assets = cfg.assets;
+  genAssetsDrv = paths: pkgs.symlinkJoin {
+    name = "dae-assets";
+    inherit paths;
+  };
 in
 {
-  meta.maintainers = with lib.maintainers; [ pokon548 ];
+  meta.maintainers = with lib.maintainers; [ pokon548 oluceps ];
 
   options = {
-    services.dae = {
-      enable = lib.options.mkEnableOption (lib.mdDoc "the dae service");
-      package = lib.mkPackageOptionMD pkgs "dae" { };
+    services.dae = with lib;{
+      enable = mkEnableOption
+        (mdDoc "A Linux high-performance transparent proxy solution based on eBPF");
+
+      package = mkPackageOptionMD pkgs "dae" { };
+
+      assets = mkOption {
+        type = with types;(listOf path);
+        default = with pkgs; [ v2ray-geoip v2ray-domain-list-community ];
+        defaultText = literalExpression "with pkgs; [ v2ray-geoip v2ray-domain-list-community ]";
+        description = mdDoc ''
+          Assets required to run dae.
+        '';
+      };
+
+      assetsPath = mkOption {
+        type = types.str;
+        default = "${genAssetsDrv assets}/share/v2ray";
+        defaultText = literalExpression ''
+          (symlinkJoin {
+              name = "dae-assets";
+              paths = assets;
+          })/share/v2ray
+        '';
+        description = mdDoc ''
+          The path which contains geolocation database.
+          This option will override `assets`.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = with types; submodule {
+          options = {
+            enable = mkEnableOption "enable";
+            port = mkOption {
+              type = types.int;
+              description = ''
+                Port to be opened. Consist with field `tproxy_port` in config file.
+              '';
+            };
+          };
+        };
+        default = {
+          enable = true;
+          port = 12345;
+        };
+        defaultText = literalExpression ''
+          {
+            enable = true;
+            port = 12345;
+          }
+        '';
+        description = mdDoc ''
+          Open the firewall port.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        default = "/etc/dae/config.dae";
+        example = "/path/to/your/config.dae";
+        description = mdDoc ''
+          The path of dae config file, end with `.dae`.
+        '';
+      };
+
+      config = mkOption {
+        type = types.str;
+        default = ''
+          global{}
+          routing{}
+        '';
+        description = mdDoc ''
+          Config text for dae.
+
+          See <https://github.com/daeuniverse/dae/blob/main/example.dae>.
+        '';
+      };
+
+      disableTxChecksumIpGeneric =
+        mkEnableOption (mdDoc "See <https://github.com/daeuniverse/dae/issues/43>");
+
     };
   };
 
-  config = lib.mkIf config.services.dae.enable {
-    networking.firewall.allowedTCPPorts = [ 12345 ];
-    networking.firewall.allowedUDPPorts = [ 12345 ];
+  config = lib.mkIf cfg.enable
+
+    {
+      environment.systemPackages = [ cfg.package ];
+      systemd.packages = [ cfg.package ];
 
-    systemd.services.dae = {
-      unitConfig = {
-        Description = "dae Service";
-        Documentation = "https://github.com/daeuniverse/dae";
-        After = [ "network-online.target" "systemd-sysctl.service" ];
-        Wants = [ "network-online.target" ];
+      environment.etc."dae/config.dae" = {
+        mode = "0400";
+        source = pkgs.writeText "config.dae" cfg.config;
       };
 
-      serviceConfig = {
-        User = "root";
-        ExecStartPre = "${lib.getExe cfg.package} validate -c /etc/dae/config.dae";
-        ExecStart = "${lib.getExe cfg.package} run --disable-timestamp -c /etc/dae/config.dae";
-        ExecReload = "${lib.getExe cfg.package} reload $MAINPID";
-        LimitNPROC = 512;
-        LimitNOFILE = 1048576;
-        Restart = "on-abnormal";
-        Type = "notify";
+      networking = lib.mkIf cfg.openFirewall.enable {
+        firewall =
+          let portToOpen = cfg.openFirewall.port;
+          in
+          {
+            allowedTCPPorts = [ portToOpen ];
+            allowedUDPPorts = [ portToOpen ];
+          };
       };
 
-      wantedBy = [ "multi-user.target" ];
+      systemd.services.dae =
+        let
+          daeBin = lib.getExe cfg.package;
+          TxChecksumIpGenericWorkaround = with lib;(getExe pkgs.writeShellApplication {
+            name = "disable-tx-checksum-ip-generic";
+            text = with pkgs; ''
+              iface=$(${iproute2}/bin/ip route | ${lib.getExe gawk} '/default/ {print $5}')
+              ${lib.getExe ethtool} -K "$iface" tx-checksum-ip-generic off
+            '';
+          });
+        in
+        {
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            ExecStartPre = [ "" "${daeBin} validate -c ${cfg.configFile}" ]
+              ++ (with lib; optional cfg.disableTxChecksumIpGeneric TxChecksumIpGenericWorkaround);
+            ExecStart = [ "" "${daeBin} run --disable-timestamp -c ${cfg.configFile}" ];
+            Environment = "DAE_LOCATION_ASSET=${cfg.assetsPath}";
+          };
+        };
+
+      assertions = [
+        {
+          assertion = lib.pathExists (toString (genAssetsDrv cfg.assets) + "/share/v2ray");
+          message = ''
+            Packages in `assets` has no preset paths included.
+            Please set `assetsPath` instead.
+          '';
+        }
+
+        {
+          assertion = !((config.services.dae.config != "global{}\nrouting{}\n")
+            && (config.services.dae.configFile != "/etc/dae/config.dae"));
+          message = ''
+            Option `config` and `configFile` could not be set
+            at the same time.
+          '';
+        }
+      ];
     };
-  };
 }
diff --git a/nixos/modules/services/networking/dnscrypt-proxy2.nix b/nixos/modules/services/networking/dnscrypt-proxy2.nix
index de1ca0d2f206..4592a0c2f6b3 100644
--- a/nixos/modules/services/networking/dnscrypt-proxy2.nix
+++ b/nixos/modules/services/networking/dnscrypt-proxy2.nix
@@ -11,7 +11,7 @@ in
     settings = mkOption {
       description = lib.mdDoc ''
         Attrset that is converted and passed as TOML config file.
-        For available params, see: <https://github.com/DNSCrypt/dnscrypt-proxy/blob/${pkgs.dnscrypt-proxy2.version}/dnscrypt-proxy/example-dnscrypt-proxy.toml>
+        For available params, see: <https://github.com/DNSCrypt/dnscrypt-proxy/blob/${pkgs.dnscrypt-proxy.version}/dnscrypt-proxy/example-dnscrypt-proxy.toml>
       '';
       example = literalExpression ''
         {
@@ -49,7 +49,7 @@ in
         passAsFile = [ "json" ];
       } ''
         ${if cfg.upstreamDefaults then ''
-          ${pkgs.remarshal}/bin/toml2json ${pkgs.dnscrypt-proxy2.src}/dnscrypt-proxy/example-dnscrypt-proxy.toml > example.json
+          ${pkgs.remarshal}/bin/toml2json ${pkgs.dnscrypt-proxy.src}/dnscrypt-proxy/example-dnscrypt-proxy.toml > example.json
           ${pkgs.jq}/bin/jq --slurp add example.json $jsonPath > config.json # merges the two
         '' else ''
           cp $jsonPath config.json
@@ -80,7 +80,7 @@ in
         AmbientCapabilities = "CAP_NET_BIND_SERVICE";
         CacheDirectory = "dnscrypt-proxy";
         DynamicUser = true;
-        ExecStart = "${pkgs.dnscrypt-proxy2}/bin/dnscrypt-proxy -config ${cfg.configFile}";
+        ExecStart = "${pkgs.dnscrypt-proxy}/bin/dnscrypt-proxy -config ${cfg.configFile}";
         LockPersonality = true;
         LogsDirectory = "dnscrypt-proxy";
         MemoryDenyWriteExecute = true;
diff --git a/nixos/modules/services/networking/firewall-nftables.nix b/nixos/modules/services/networking/firewall-nftables.nix
index 452dd97d89d2..7c7136cc96f1 100644
--- a/nixos/modules/services/networking/firewall-nftables.nix
+++ b/nixos/modules/services/networking/firewall-nftables.nix
@@ -70,10 +70,8 @@ in
       }
     ];
 
-    networking.nftables.ruleset = ''
-
-      table inet nixos-fw {
-
+    networking.nftables.tables."nixos-fw".family = "inet";
+    networking.nftables.tables."nixos-fw".content = ''
         ${optionalString (cfg.checkReversePath != false) ''
           chain rpfilter {
             type filter hook prerouting priority mangle + 10; policy drop;
@@ -169,9 +167,6 @@ in
 
           }
         ''}
-
-      }
-
     '';
 
   };
diff --git a/nixos/modules/services/networking/jool.nix b/nixos/modules/services/networking/jool.nix
new file mode 100644
index 000000000000..d2d2b0956e8a
--- /dev/null
+++ b/nixos/modules/services/networking/jool.nix
@@ -0,0 +1,281 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.networking.jool;
+
+  jool = config.boot.kernelPackages.jool;
+  jool-cli = pkgs.jool-cli;
+
+  hardening = {
+    # Run as unprivileged user
+    User = "jool";
+    Group = "jool";
+    DynamicUser = true;
+
+    # Restrict filesystem to only read the jool module
+    TemporaryFileSystem = [ "/" ];
+    BindReadOnlyPaths = [
+      builtins.storeDir
+      "/run/booted-system/kernel-modules"
+    ];
+
+    # Give capabilities to load the module and configure it
+    AmbientCapabilities = [ "CAP_SYS_MODULE" "CAP_NET_ADMIN" ];
+    RestrictAddressFamilies = [ "AF_NETLINK" ];
+
+    # Other restrictions
+    RestrictNamespaces = [ "net" ];
+    SystemCallFilter = [ "@system-service" "@module" ];
+    CapabilityBoundingSet = [ "CAP_SYS_MODULE" "CAP_NET_ADMIN" ];
+  };
+
+  configFormat = pkgs.formats.json {};
+
+  # Generate the config file of instance `name`
+  nat64Conf = name:
+    configFormat.generate "jool-nat64-${name}.conf"
+      (cfg.nat64.${name} // { instance = name; });
+  siitConf = name:
+    configFormat.generate "jool-siit-${name}.conf"
+      (cfg.siit.${name} // { instance = name; });
+
+  # NAT64 config type
+  nat64Options = lib.types.submodule {
+    # The format is plain JSON
+    freeformType = configFormat.type;
+    # Some options with a default value
+    options.framework = lib.mkOption {
+      type = lib.types.enum [ "netfilter" "iptables" ];
+      default = "netfilter";
+      description = lib.mdDoc ''
+        The framework to use for attaching Jool's translation to the exist
+        kernel packet processing rules. See the
+        [documentation](https://nicmx.github.io/Jool/en/intro-jool.html#design)
+        for the differences between the two options.
+      '';
+    };
+    options.global.pool6 = lib.mkOption {
+      type = lib.types.strMatching "[[:xdigit:]:]+/[[:digit:]]+"
+        // { description = "Network prefix in CIDR notation"; };
+      default = "64:ff9b::/96";
+      description = lib.mdDoc ''
+        The prefix used for embedding IPv4 into IPv6 addresses.
+        Defaults to the well-known NAT64 prefix, defined by
+        [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052).
+      '';
+    };
+  };
+
+  # SIIT config type
+  siitOptions = lib.types.submodule {
+    # The format is, again, plain JSON
+    freeformType = configFormat.type;
+    # Some options with a default value
+    options = { inherit (nat64Options.getSubOptions []) framework; };
+  };
+
+  makeNat64Unit = name: opts: {
+    "jool-nat64-${name}" = {
+      description = "Jool, NAT64 setup of instance ${name}";
+      documentation = [ "https://nicmx.github.io/Jool/en/documentation.html" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStartPre = "${pkgs.kmod}/bin/modprobe jool";
+        ExecStart    = "${jool-cli}/bin/jool file handle ${nat64Conf name}";
+        ExecStop     = "${jool-cli}/bin/jool -f ${nat64Conf name} instance remove";
+      } // hardening;
+    };
+  };
+
+  makeSiitUnit = name: opts: {
+    "jool-siit-${name}" = {
+      description = "Jool, SIIT setup of instance ${name}";
+      documentation = [ "https://nicmx.github.io/Jool/en/documentation.html" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStartPre = "${pkgs.kmod}/bin/modprobe jool_siit";
+        ExecStart    = "${jool-cli}/bin/jool_siit file handle ${siitConf name}";
+        ExecStop     = "${jool-cli}/bin/jool_siit -f ${siitConf name} instance remove";
+      } // hardening;
+    };
+  };
+
+  checkNat64 = name: _: ''
+    printf 'Validating Jool configuration for NAT64 instance "${name}"... '
+    jool file check ${nat64Conf name}
+    printf 'Ok.\n'; touch "$out"
+  '';
+
+  checkSiit = name: _: ''
+    printf 'Validating Jool configuration for SIIT instance "${name}"... '
+    jool_siit file check ${siitConf name}
+    printf 'Ok.\n'; touch "$out"
+  '';
+
+in
+
+{
+  options = {
+    networking.jool.enable = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      relatedPackages = [ "linuxPackages.jool" "jool-cli" ];
+      description = lib.mdDoc ''
+        Whether to enable Jool, an Open Source implementation of IPv4/IPv6
+        translation on Linux.
+
+        Jool can perform stateless IP/ICMP translation (SIIT) or stateful
+        NAT64, analogous to the IPv4 NAPT. Refer to the upstream
+        [documentation](https://nicmx.github.io/Jool/en/intro-xlat.html) for
+        the supported modes of translation and how to configure them.
+
+        Enabling this option will install the Jool kernel module and the
+        command line tools for controlling it.
+      '';
+    };
+
+    networking.jool.nat64 = lib.mkOption {
+      type = lib.types.attrsOf nat64Options;
+      default = { };
+      example = lib.literalExpression ''
+        {
+          default = {
+            # custom NAT64 prefix
+            global.pool6 = "2001:db8:64::/96";
+
+            # Port forwarding
+            bib = [
+              { # SSH 192.0.2.16 → 2001:db8:a::1
+                "protocol"     = "TCP";
+                "ipv4 address" = "192.0.2.16#22";
+                "ipv6 address" = "2001:db8:a::1#22";
+              }
+              { # DNS (TCP) 192.0.2.16 → 2001:db8:a::2
+                "protocol"     = "TCP";
+                "ipv4 address" = "192.0.2.16#53";
+                "ipv6 address" = "2001:db8:a::2#53";
+              }
+              { # DNS (UDP) 192.0.2.16 → 2001:db8:a::2
+                "protocol" = "UDP";
+                "ipv4 address" = "192.0.2.16#53";
+                "ipv6 address" = "2001:db8:a::2#53";
+              }
+            ];
+
+            pool4 = [
+              # Port ranges for dynamic translation
+              { protocol =  "TCP";  prefix = "192.0.2.16/32"; "port range" = "40001-65535"; }
+              { protocol =  "UDP";  prefix = "192.0.2.16/32"; "port range" = "40001-65535"; }
+              { protocol = "ICMP";  prefix = "192.0.2.16/32"; "port range" = "40001-65535"; }
+
+              # Ports for static BIB entries
+              { protocol =  "TCP";  prefix = "192.0.2.16/32"; "port range" = "22"; }
+              { protocol =  "UDP";  prefix = "192.0.2.16/32"; "port range" = "53"; }
+            ];
+          };
+        }
+      '';
+      description = lib.mdDoc ''
+        Definitions of NAT64 instances of Jool.
+        See the
+        [documentation](https://nicmx.github.io/Jool/en/config-atomic.html) for
+        the available options. Also check out the
+        [tutorial](https://nicmx.github.io/Jool/en/run-nat64.html) for an
+        introduction to NAT64 and how to troubleshoot the setup.
+
+        The attribute name defines the name of the instance, with the main one
+        being `default`: this can be accessed from the command line without
+        specifying the name with `-i`.
+
+        ::: {.note}
+        Instances created imperatively from the command line will not interfere
+        with the NixOS instances, provided the respective `pool4` addresses and
+        port ranges are not overlapping.
+        :::
+
+        ::: {.warning}
+        Changes to an instance performed via `jool -i <name>` are applied
+        correctly but will be lost after restarting the respective
+        `jool-nat64-<name>.service`.
+        :::
+      '';
+    };
+
+    networking.jool.siit = lib.mkOption {
+      type = lib.types.attrsOf siitOptions;
+      default = { };
+      example = lib.literalExpression ''
+        {
+          default = {
+            # Maps any IPv4 address x.y.z.t to 2001:db8::x.y.z.t and v.v.
+            global.pool6 = "2001:db8::/96";
+
+            # Explicit address mappings
+            eamt = [
+              # 2001:db8:1:: ←→ 192.0.2.0
+              { "ipv6 prefix" = "2001:db8:1::/128"; "ipv4 prefix" = "192.0.2.0"; }
+              # 2001:db8:1::x ←→ 198.51.100.x
+              { "ipv6 prefix" = "2001:db8:2::/120"; "ipv4 prefix" = "198.51.100.0/24"; }
+            ];
+          };
+        }
+      '';
+      description = lib.mdDoc ''
+        Definitions of SIIT instances of Jool.
+        See the
+        [documentation](https://nicmx.github.io/Jool/en/config-atomic.html) for
+        the available options. Also check out the
+        [tutorial](https://nicmx.github.io/Jool/en/run-vanilla.html) for an
+        introduction to SIIT and how to troubleshoot the setup.
+
+        The attribute name defines the name of the instance, with the main one
+        being `default`: this can be accessed from the command line without
+        specifying the name with `-i`.
+
+        ::: {.note}
+        Instances created imperatively from the command line will not interfere
+        with the NixOS instances, provided the respective EAMT addresses and
+        port ranges are not overlapping.
+        :::
+
+        ::: {.warning}
+        Changes to an instance performed via `jool -i <name>` are applied
+        correctly but will be lost after restarting the respective
+        `jool-siit-<name>.service`.
+        :::
+      '';
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+    # Install kernel module and cli tools
+    boot.extraModulePackages = [ jool ];
+    environment.systemPackages = [ jool-cli ];
+
+    # Install services for each instance
+    systemd.services = lib.mkMerge
+      (lib.mapAttrsToList makeNat64Unit cfg.nat64 ++
+       lib.mapAttrsToList makeSiitUnit cfg.siit);
+
+    # Check the configuration of each instance
+    system.checks = lib.optional (cfg.nat64 != {} || cfg.siit != {})
+      (pkgs.runCommand "jool-validated"
+        {
+          nativeBuildInputs = with pkgs.buildPackages; [ jool-cli ];
+          preferLocalBuild = true;
+        }
+        (lib.concatStrings
+          (lib.mapAttrsToList checkNat64 cfg.nat64 ++
+           lib.mapAttrsToList checkSiit cfg.siit)));
+  };
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+}
diff --git a/nixos/modules/services/networking/nat-nftables.nix b/nixos/modules/services/networking/nat-nftables.nix
index 483910a16658..4b2317ca2ffc 100644
--- a/nixos/modules/services/networking/nat-nftables.nix
+++ b/nixos/modules/services/networking/nat-nftables.nix
@@ -145,28 +145,28 @@ in
       }
     ];
 
-    networking.nftables.ruleset = ''
-      table ip nixos-nat {
-        ${mkTable {
+    networking.nftables.tables = {
+      "nixos-nat" = {
+        family = "ip";
+        content = mkTable {
           ipVer = "ip";
           inherit dest ipSet;
           forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
           inherit (cfg) dmzHost;
-        }}
-      }
-
-      ${optionalString cfg.enableIPv6 ''
-        table ip6 nixos-nat {
-          ${mkTable {
-            ipVer = "ip6";
-            dest = destIPv6;
-            ipSet = ipv6Set;
-            forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
-            dmzHost = null;
-          }}
-        }
-      ''}
-    '';
+        };
+      };
+      "nixos-nat6" = mkIf cfg.enableIPv6 {
+        family = "ip6";
+        name = "nixos-nat";
+        content = mkTable {
+          ipVer = "ip6";
+          dest = destIPv6;
+          ipSet = ipv6Set;
+          forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
+          dmzHost = null;
+        };
+      };
+    };
 
     networking.firewall.extraForwardRules = optionalString config.networking.firewall.filterForward ''
       ${optionalString (ifaceSet != "") ''
diff --git a/nixos/modules/services/networking/nftables.nix b/nixos/modules/services/networking/nftables.nix
index faff1dca89ba..47159ade328c 100644
--- a/nixos/modules/services/networking/nftables.nix
+++ b/nixos/modules/services/networking/nftables.nix
@@ -2,6 +2,35 @@
 with lib;
 let
   cfg = config.networking.nftables;
+
+  tableSubmodule = { name, ... }: {
+    options = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Enable this table.";
+      };
+
+      name = mkOption {
+        type = types.str;
+        description = lib.mdDoc "Table name.";
+      };
+
+      content = mkOption {
+        type = types.lines;
+        description = lib.mdDoc "The table content.";
+      };
+
+      family = mkOption {
+        description = lib.mdDoc "Table family.";
+        type = types.enum [ "ip" "ip6" "inet" "arp" "bridge" "netdev" ];
+      };
+    };
+
+    config = {
+      name = mkDefault name;
+    };
+  };
 in
 {
   ###### interface
@@ -41,6 +70,26 @@ in
       '';
     };
 
+    networking.nftables.checkRulesetRedirects = mkOption {
+      type = types.addCheck (types.attrsOf types.path) (attrs: all types.path.check (attrNames attrs));
+      default = {
+        "/etc/hosts" = config.environment.etc.hosts.source;
+        "/etc/protocols" = config.environment.etc.protocols.source;
+        "/etc/services" = config.environment.etc.services.source;
+      };
+      defaultText = literalExpression ''
+        {
+          "/etc/hosts" = config.environment.etc.hosts.source;
+          "/etc/protocols" = config.environment.etc.protocols.source;
+          "/etc/services" = config.environment.etc.services.source;
+        }
+      '';
+      description = mdDoc ''
+        Set of paths that should be intercepted and rewritten while checking the ruleset
+        using `pkgs.buildPackages.libredirect`.
+      '';
+    };
+
     networking.nftables.preCheckRuleset = mkOption {
       type = types.lines;
       default = "";
@@ -54,6 +103,24 @@ in
       '';
     };
 
+    networking.nftables.flushRuleset = mkEnableOption (lib.mdDoc "Flush the entire ruleset on each reload.");
+
+    networking.nftables.extraDeletions = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        # this makes deleting a non-existing table a no-op instead of an error
+        table inet some-table;
+
+        delete table inet some-table;
+      '';
+      description =
+        lib.mdDoc ''
+          Extra deletion commands to be run on every firewall start, reload
+          and after stopping the firewall.
+        '';
+    };
+
     networking.nftables.ruleset = mkOption {
       type = types.lines;
       default = "";
@@ -103,7 +170,10 @@ in
         lib.mdDoc ''
           The ruleset to be used with nftables.  Should be in a format that
           can be loaded using "/bin/nft -f".  The ruleset is updated atomically.
-          This option conflicts with rulesetFile.
+          Note that if the tables should be cleaned first, either:
+          - networking.nftables.flushRuleset = true; needs to be set (flushes all tables)
+          - networking.nftables.extraDeletions needs to be set
+          - or networking.nftables.tables can be used, which will clean up the table automatically
         '';
     };
     networking.nftables.rulesetFile = mkOption {
@@ -113,9 +183,64 @@ in
         lib.mdDoc ''
           The ruleset file to be used with nftables.  Should be in a format that
           can be loaded using "nft -f".  The ruleset is updated atomically.
-          This option conflicts with ruleset and nftables based firewall.
         '';
     };
+    networking.nftables.tables = mkOption {
+      type = types.attrsOf (types.submodule tableSubmodule);
+
+      default = {};
+
+      description = lib.mdDoc ''
+        Tables to be added to ruleset.
+        Tables will be added together with delete statements to clean up the table before every update.
+      '';
+
+      example = {
+        filter = {
+          family = "inet";
+          content = ''
+            # Check out https://wiki.nftables.org/ for better documentation.
+            # Table for both IPv4 and IPv6.
+            # Block all incoming connections traffic except SSH and "ping".
+            chain input {
+              type filter hook input priority 0;
+
+              # accept any localhost traffic
+              iifname lo accept
+
+              # accept traffic originated from us
+              ct state {established, related} accept
+
+              # ICMP
+              # routers may also want: mld-listener-query, nd-router-solicit
+              ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
+              ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept
+
+              # allow "ping"
+              ip6 nexthdr icmpv6 icmpv6 type echo-request accept
+              ip protocol icmp icmp type echo-request accept
+
+              # accept SSH connections (required for a server)
+              tcp dport 22 accept
+
+              # count and drop any other traffic
+              counter drop
+            }
+
+            # Allow all outgoing connections.
+            chain output {
+              type filter hook output priority 0;
+              accept
+            }
+
+            chain forward {
+              type filter hook forward priority 0;
+              accept
+            }
+          '';
+        };
+      };
+    };
   };
 
   ###### implementation
@@ -124,6 +249,8 @@ in
     boot.blacklistedKernelModules = [ "ip_tables" ];
     environment.systemPackages = [ pkgs.nftables ];
     networking.networkmanager.firewallBackend = mkDefault "nftables";
+    # versionOlder for backportability, remove afterwards
+    networking.nftables.flushRuleset = mkDefault (versionOlder config.system.stateVersion "23.11" || (cfg.rulesetFile != null || cfg.ruleset != ""));
     systemd.services.nftables = {
       description = "nftables firewall";
       before = [ "network-pre.target" ];
@@ -131,20 +258,51 @@ in
       wantedBy = [ "multi-user.target" ];
       reloadIfChanged = true;
       serviceConfig = let
+        enabledTables = filterAttrs (_: table: table.enable) cfg.tables;
+        deletionsScript = pkgs.writeScript "nftables-deletions" ''
+          #! ${pkgs.nftables}/bin/nft -f
+          ${if cfg.flushRuleset then "flush ruleset"
+            else concatStringsSep "\n" (mapAttrsToList (_: table: ''
+              table ${table.family} ${table.name}
+              delete table ${table.family} ${table.name}
+            '') enabledTables)}
+          ${cfg.extraDeletions}
+        '';
+        deletionsScriptVar = "/var/lib/nftables/deletions.nft";
+        ensureDeletions = pkgs.writeShellScript "nftables-ensure-deletions" ''
+          touch ${deletionsScriptVar}
+          chmod +x ${deletionsScriptVar}
+        '';
+        saveDeletionsScript = pkgs.writeShellScript "nftables-save-deletions" ''
+          cp ${deletionsScript} ${deletionsScriptVar}
+        '';
+        cleanupDeletionsScript = pkgs.writeShellScript "nftables-cleanup-deletions" ''
+          rm ${deletionsScriptVar}
+        '';
         rulesScript = pkgs.writeTextFile {
           name =  "nftables-rules";
           executable = true;
           text = ''
             #! ${pkgs.nftables}/bin/nft -f
-            flush ruleset
-            ${if cfg.rulesetFile != null then ''
+            # previous deletions, if any
+            include "${deletionsScriptVar}"
+            # current deletions
+            include "${deletionsScript}"
+            ${concatStringsSep "\n" (mapAttrsToList (_: table: ''
+              table ${table.family} ${table.name} {
+                ${table.content}
+              }
+            '') enabledTables)}
+            ${cfg.ruleset}
+            ${lib.optionalString (cfg.rulesetFile != null) ''
               include "${cfg.rulesetFile}"
-            '' else cfg.ruleset}
+            ''}
           '';
           checkPhase = lib.optionalString cfg.checkRuleset ''
             cp $out ruleset.conf
+            sed 's|include "${deletionsScriptVar}"||' -i ruleset.conf
             ${cfg.preCheckRuleset}
-            export NIX_REDIRECTS=/etc/protocols=${pkgs.buildPackages.iana-etc}/etc/protocols:/etc/services=${pkgs.buildPackages.iana-etc}/etc/services
+            export NIX_REDIRECTS=${escapeShellArg (concatStringsSep ":" (mapAttrsToList (n: v: "${n}=${v}") cfg.checkRulesetRedirects))}
             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
           '';
@@ -152,9 +310,11 @@ in
       in {
         Type = "oneshot";
         RemainAfterExit = true;
-        ExecStart = rulesScript;
-        ExecReload = rulesScript;
-        ExecStop = "${pkgs.nftables}/bin/nft flush ruleset";
+        ExecStart = [ ensureDeletions rulesScript ];
+        ExecStartPost = saveDeletionsScript;
+        ExecReload = [ ensureDeletions rulesScript saveDeletionsScript ];
+        ExecStop = [ deletionsScriptVar cleanupDeletionsScript ];
+        StateDirectory = "nftables";
       };
     };
   };
diff --git a/nixos/modules/services/networking/nncp.nix b/nixos/modules/services/networking/nncp.nix
new file mode 100644
index 000000000000..3cfe41995e76
--- /dev/null
+++ b/nixos/modules/services/networking/nncp.nix
@@ -0,0 +1,131 @@
+{ config, lib, pkgs, ... }:
+with lib;
+
+let
+  nncpCfgFile = "/run/nncp.hjson";
+  programCfg = config.programs.nncp;
+  callerCfg = config.services.nncp.caller;
+  daemonCfg = config.services.nncp.daemon;
+  settingsFormat = pkgs.formats.json { };
+  jsonCfgFile = settingsFormat.generate "nncp.json" programCfg.settings;
+  pkg = programCfg.package;
+in {
+  options = {
+
+    services.nncp = {
+      caller = {
+        enable = mkEnableOption ''
+          cron'ed NNCP TCP daemon caller.
+          The daemon will take configuration from
+          [](#opt-programs.nncp.settings)
+        '';
+        extraArgs = mkOption {
+          type = with types; listOf str;
+          description = "Extra command-line arguments to pass to caller.";
+          default = [ ];
+          example = [ "-autotoss" ];
+        };
+      };
+
+      daemon = {
+        enable = mkEnableOption ''
+          NNCP TCP synronization daemon.
+          The daemon will take configuration from
+          [](#opt-programs.nncp.settings)
+        '';
+        socketActivation = {
+          enable = mkEnableOption ''
+            Whether to run nncp-daemon persistently or socket-activated.
+          '';
+          listenStreams = mkOption {
+            type = with types; listOf str;
+            description = lib.mdDoc ''
+              TCP sockets to bind to.
+              See [](#opt-systemd.sockets._name_.listenStreams).
+            '';
+            default = [ "5400" ];
+          };
+        };
+        extraArgs = mkOption {
+          type = with types; listOf str;
+          description = "Extra command-line arguments to pass to daemon.";
+          default = [ ];
+          example = [ "-autotoss" ];
+        };
+      };
+
+    };
+  };
+
+  config = mkIf (programCfg.enable or callerCfg.enable or daemonCfg.enable) {
+
+    assertions = [{
+      assertion = with builtins;
+        let
+          callerCongfigured =
+            let neigh = config.programs.nncp.settings.neigh or { };
+            in lib.lists.any (x: hasAttr "calls" x && x.calls != [ ])
+            (attrValues neigh);
+        in !callerCfg.enable || callerCongfigured;
+      message = "NNCP caller enabled but call configuration is missing";
+    }];
+
+    systemd.services."nncp-caller" = {
+      inherit (callerCfg) enable;
+      description = "Croned NNCP TCP daemon caller.";
+      documentation = [ "http://www.nncpgo.org/nncp_002dcaller.html" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkg}/bin/nncp-caller -noprogress -cfg "${nncpCfgFile}" ${
+            lib.strings.escapeShellArgs callerCfg.extraArgs
+          }'';
+        Group = "uucp";
+        UMask = "0002";
+      };
+    };
+
+    systemd.services."nncp-daemon" = mkIf daemonCfg.enable {
+      enable = !daemonCfg.socketActivation.enable;
+      description = "NNCP TCP syncronization daemon.";
+      documentation = [ "http://www.nncpgo.org/nncp_002ddaemon.html" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkg}/bin/nncp-daemon -noprogress -cfg "${nncpCfgFile}" ${
+            lib.strings.escapeShellArgs daemonCfg.extraArgs
+          }'';
+        Restart = "on-failure";
+        Group = "uucp";
+        UMask = "0002";
+      };
+    };
+
+    systemd.services."nncp-daemon@" = mkIf daemonCfg.socketActivation.enable {
+      description = "NNCP TCP syncronization daemon.";
+      documentation = [ "http://www.nncpgo.org/nncp_002ddaemon.html" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkg}/bin/nncp-daemon -noprogress -ucspi -cfg "${nncpCfgFile}" ${
+            lib.strings.escapeShellArgs daemonCfg.extraArgs
+          }'';
+        Group = "uucp";
+        UMask = "0002";
+        StandardInput = "socket";
+        StandardOutput = "inherit";
+        StandardError = "journal";
+      };
+    };
+
+    systemd.sockets.nncp-daemon = mkIf daemonCfg.socketActivation.enable {
+      inherit (daemonCfg.socketActivation) listenStreams;
+      description = "socket for NNCP TCP syncronization.";
+      conflicts = [ "nncp-daemon.service" ];
+      wantedBy = [ "sockets.target" ];
+      socketConfig.Accept = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/privoxy.nix b/nixos/modules/services/networking/privoxy.nix
index 78d02aaa1125..619490a4c020 100644
--- a/nixos/modules/services/networking/privoxy.nix
+++ b/nixos/modules/services/networking/privoxy.nix
@@ -12,7 +12,7 @@ let
     else "${name} ${toString val}\n";
 
   configType = with types;
-    let atom = oneOf [ int bool string path ];
+    let atom = oneOf [ int bool str path ];
     in attrsOf (either atom (listOf atom))
     // { description = ''
           privoxy configuration type. The format consists of an attribute
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
index f308b7e33114..8b35cc8d6669 100644
--- a/nixos/modules/services/networking/tailscale.nix
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -6,7 +6,7 @@ let
   cfg = config.services.tailscale;
   isNetworkd = config.networking.useNetworkd;
 in {
-  meta.maintainers = with maintainers; [ danderson mbaillie twitchyliquid64 ];
+  meta.maintainers = with maintainers; [ danderson mbaillie twitchyliquid64 mfrw ];
 
   options.services.tailscale = {
     enable = mkEnableOption (lib.mdDoc "Tailscale client daemon");
diff --git a/nixos/modules/services/networking/twingate.nix b/nixos/modules/services/networking/twingate.nix
index 170d392bf213..03c68fc874f0 100644
--- a/nixos/modules/services/networking/twingate.nix
+++ b/nixos/modules/services/networking/twingate.nix
@@ -17,7 +17,7 @@ in
     };
 
     networking.firewall.checkReversePath = lib.mkDefault "loose";
-    services.resolved.enable = !(config.networking.networkmanager.enable);
+    services.resolved.enable = lib.mkIf (!config.networking.networkmanager.enable) true;
 
     environment.systemPackages = [ cfg.package ]; # For the CLI.
   };
diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix
index d8a99dee59f4..6f4d1dc382ab 100644
--- a/nixos/modules/services/security/kanidm.nix
+++ b/nixos/modules/services/security/kanidm.nix
@@ -137,7 +137,7 @@ in
       default = { };
       description = lib.mdDoc ''
         Settings for Kanidm, see
-        [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/server_configuration.md)
+        [the documentation](https://kanidm.github.io/kanidm/stable/server_configuration.html)
         and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/server.toml)
         for possible values.
       '';
@@ -155,7 +155,7 @@ in
       };
       description = lib.mdDoc ''
         Configure Kanidm clients, needed for the PAM daemon. See
-        [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/client_tools.md#kanidm-configuration)
+        [the documentation](https://kanidm.github.io/kanidm/stable/client_tools.html#kanidm-configuration)
         and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/config)
         for possible values.
       '';
@@ -173,7 +173,7 @@ in
       };
       description = lib.mdDoc ''
         Configure Kanidm unix daemon.
-        See [the documentation](https://github.com/kanidm/kanidm/blob/master/kanidm_book/src/pam_and_nsswitch.md#the-unix-daemon)
+        See [the documentation](https://kanidm.github.io/kanidm/stable/integrations/pam_and_nsswitch.html#the-unix-daemon)
         and [example configuration](https://github.com/kanidm/kanidm/blob/master/examples/unixd)
         for possible values.
       '';
diff --git a/nixos/modules/services/system/zram-generator.nix b/nixos/modules/services/system/zram-generator.nix
new file mode 100644
index 000000000000..5902eda55696
--- /dev/null
+++ b/nixos/modules/services/system/zram-generator.nix
@@ -0,0 +1,38 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.zram-generator;
+  settingsFormat = pkgs.formats.ini { };
+in
+{
+  meta = {
+    maintainers = with lib.maintainers; [ nickcao ];
+  };
+
+  options.services.zram-generator = {
+    enable = lib.mkEnableOption (lib.mdDoc "Systemd unit generator for zram devices");
+
+    package = lib.mkPackageOptionMD pkgs "zram-generator" { };
+
+    settings = lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+      };
+      default = { };
+      description = lib.mdDoc ''
+        Configuration for zram-generator,
+        see https://github.com/systemd/zram-generator for documentation.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    system.requiredKernelConfig = with config.lib.kernelConfig; [
+      (isModule "ZRAM")
+    ];
+
+    systemd.packages = [ cfg.package ];
+    systemd.services."systemd-zram-setup@".path = [ pkgs.util-linux ]; # for mkswap
+
+    environment.etc."systemd/zram-generator.conf".source = settingsFormat.generate "zram-generator.conf" cfg.settings;
+  };
+}
diff --git a/nixos/modules/services/web-apps/cloudlog.nix b/nixos/modules/services/web-apps/cloudlog.nix
index 9261de8d4354..da2cf93d7f1c 100644
--- a/nixos/modules/services/web-apps/cloudlog.nix
+++ b/nixos/modules/services/web-apps/cloudlog.nix
@@ -308,8 +308,6 @@ in
       pools.cloudlog = {
         inherit (cfg) user;
         group = config.services.nginx.group;
-        # cloudlog is currently broken on php 8.2
-        phpPackage = pkgs.php81;
         settings =  {
           "listen.owner" = config.services.nginx.user;
           "listen.group" = config.services.nginx.group;
diff --git a/nixos/modules/services/web-apps/honk.md b/nixos/modules/services/web-apps/honk.md
new file mode 100644
index 000000000000..f34085f7dc52
--- /dev/null
+++ b/nixos/modules/services/web-apps/honk.md
@@ -0,0 +1,23 @@
+# Honk {#module-services-honk}
+
+With Honk on NixOS you can quickly configure a complete ActivityPub server with
+minimal setup and support costs.
+
+## Basic usage {#module-services-honk-basic-usage}
+
+A minimal configuration looks like this:
+
+```nix
+{
+  services.honk = {
+    enable = true;
+    host = "0.0.0.0";
+    port = 8080;
+    username = "username";
+    passwordFile = "/etc/honk/password.txt";
+    servername = "honk.example.com";
+  };
+
+  networking.firewall.allowedTCPPorts = [ 8080 ];
+}
+```
diff --git a/nixos/modules/services/web-apps/honk.nix b/nixos/modules/services/web-apps/honk.nix
new file mode 100644
index 000000000000..e8718774575b
--- /dev/null
+++ b/nixos/modules/services/web-apps/honk.nix
@@ -0,0 +1,153 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+let
+  cfg = config.services.honk;
+
+  honk-initdb-script = cfg: pkgs.writeShellApplication {
+    name = "honk-initdb-script";
+
+    runtimeInputs = with pkgs; [ coreutils ];
+
+    text = ''
+      PW=$(cat "$CREDENTIALS_DIRECTORY/honk_passwordFile")
+
+      echo -e "${cfg.username}\n''$PW\n${cfg.host}:${toString cfg.port}\n${cfg.servername}" | ${lib.getExe cfg.package} -datadir "$STATE_DIRECTORY" init
+    '';
+  };
+in
+{
+  options = {
+    services.honk = {
+      enable = lib.mkEnableOption (lib.mdDoc "the Honk server");
+      package = lib.mkPackageOptionMD pkgs "honk" { };
+
+      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 = 8080;
+        description = lib.mdDoc ''
+          The port the server should listen to.
+        '';
+        type = lib.types.port;
+      };
+
+      username = lib.mkOption {
+        description = lib.mdDoc ''
+          The admin account username.
+        '';
+        type = lib.types.str;
+      };
+
+      passwordFile = lib.mkOption {
+        description = lib.mdDoc ''
+          Password for admin account.
+          NOTE: Should be string not a store path, to prevent the password from being world readable
+        '';
+        type = lib.types.path;
+      };
+
+      servername = lib.mkOption {
+        description = lib.mdDoc ''
+          The server name.
+        '';
+        type = lib.types.str;
+      };
+
+      extraJS = lib.mkOption {
+        default = null;
+        description = lib.mdDoc ''
+          An extra JavaScript file to be loaded by the client.
+        '';
+        type = lib.types.nullOr lib.types.path;
+      };
+
+      extraCSS = lib.mkOption {
+        default = null;
+        description = lib.mdDoc ''
+          An extra CSS file to be loaded by the client.
+        '';
+        type = lib.types.nullOr lib.types.path;
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.username or "" != "";
+        message = ''
+          You have to define a username for Honk (`services.honk.username`).
+        '';
+      }
+      {
+        assertion = cfg.servername or "" != "";
+        message = ''
+          You have to define a servername for Honk (`services.honk.servername`).
+        '';
+      }
+    ];
+
+    systemd.services.honk-initdb = {
+      description = "Honk server database setup";
+      requiredBy = [ "honk.service" ];
+      before = [ "honk.service" ];
+
+      serviceConfig = {
+        LoadCredential = [
+          "honk_passwordFile:${cfg.passwordFile}"
+        ];
+        Type = "oneshot";
+        StateDirectory = "honk";
+        DynamicUser = true;
+        RemainAfterExit = true;
+        ExecStart = lib.getExe (honk-initdb-script cfg);
+        PrivateTmp = true;
+      };
+
+      unitConfig = {
+        ConditionPathExists = [
+          # Skip this service if the database already exists
+          "!$STATE_DIRECTORY/honk.db"
+        ];
+      };
+    };
+
+    systemd.services.honk = {
+      description = "Honk server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      bindsTo = [ "honk-initdb.service" ];
+      preStart = ''
+        mkdir -p $STATE_DIRECTORY/views
+        ${lib.optionalString (cfg.extraJS != null) "ln -fs ${cfg.extraJS} $STATE_DIRECTORY/views/local.js"}
+        ${lib.optionalString (cfg.extraCSS != null) "ln -fs ${cfg.extraCSS} $STATE_DIRECTORY/views/local.css"}
+        ${lib.getExe cfg.package} -datadir $STATE_DIRECTORY -viewdir ${cfg.package}/share/honk backup $STATE_DIRECTORY/backup
+        ${lib.getExe cfg.package} -datadir $STATE_DIRECTORY -viewdir ${cfg.package}/share/honk upgrade
+        ${lib.getExe cfg.package} -datadir $STATE_DIRECTORY -viewdir ${cfg.package}/share/honk cleanup
+      '';
+      serviceConfig = {
+        ExecStart = ''
+          ${lib.getExe cfg.package} -datadir $STATE_DIRECTORY -viewdir ${cfg.package}/share/honk
+        '';
+        StateDirectory = "honk";
+        DynamicUser = true;
+        PrivateTmp = "yes";
+        Restart = "on-failure";
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ drupol ];
+    doc = ./honk.md;
+  };
+}
diff --git a/nixos/modules/services/web-apps/lemmy.nix b/nixos/modules/services/web-apps/lemmy.nix
index 895f3a9f1b4b..20d9dcb7c266 100644
--- a/nixos/modules/services/web-apps/lemmy.nix
+++ b/nixos/modules/services/web-apps/lemmy.nix
@@ -160,7 +160,7 @@ in
               root * ${cfg.ui.package}/dist
               file_server
             }
-            handle_path /static/undefined/* {
+            handle_path /static/${cfg.ui.package.passthru.commit_sha}/* {
               root * ${cfg.ui.package}/dist
               file_server
             }
diff --git a/nixos/modules/services/web-apps/netbox.nix b/nixos/modules/services/web-apps/netbox.nix
index e2ef350ba4e5..6d89ffc2a7b7 100644
--- a/nixos/modules/services/web-apps/netbox.nix
+++ b/nixos/modules/services/web-apps/netbox.nix
@@ -1,7 +1,5 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   cfg = config.services.netbox;
   pythonFmt = pkgs.formats.pythonVars {};
@@ -17,7 +15,7 @@ let
   pkg = (cfg.package.overrideAttrs (old: {
     installPhase = old.installPhase + ''
       ln -s ${configFile} $out/opt/netbox/netbox/netbox/configuration.py
-    '' + optionalString cfg.enableLdap ''
+    '' + lib.optionalString cfg.enableLdap ''
       ln -s ${cfg.ldapConfigPath} $out/opt/netbox/netbox/netbox/ldap_config.py
     '';
   })).override {
@@ -31,7 +29,7 @@ let
 
 in {
   options.services.netbox = {
-    enable = mkOption {
+    enable = lib.mkOption {
       type = lib.types.bool;
       default = false;
       description = lib.mdDoc ''
@@ -66,18 +64,18 @@ in {
       };
     };
 
-    listenAddress = mkOption {
-      type = types.str;
+    listenAddress = lib.mkOption {
+      type = lib.types.str;
       default = "[::1]";
       description = lib.mdDoc ''
         Address the server will listen on.
       '';
     };
 
-    package = mkOption {
-      type = types.package;
-      default = if versionAtLeast config.system.stateVersion "23.05" then pkgs.netbox else pkgs.netbox_3_3;
-      defaultText = literalExpression ''
+    package = lib.mkOption {
+      type = lib.types.package;
+      default = if lib.versionAtLeast config.system.stateVersion "23.05" then pkgs.netbox else pkgs.netbox_3_3;
+      defaultText = lib.literalExpression ''
         if versionAtLeast config.system.stateVersion "23.05" then pkgs.netbox else pkgs.netbox_3_3;
       '';
       description = lib.mdDoc ''
@@ -85,18 +83,18 @@ in {
       '';
     };
 
-    port = mkOption {
-      type = types.port;
+    port = lib.mkOption {
+      type = lib.types.port;
       default = 8001;
       description = lib.mdDoc ''
         Port the server will listen on.
       '';
     };
 
-    plugins = mkOption {
-      type = types.functionTo (types.listOf types.package);
+    plugins = lib.mkOption {
+      type = with lib.types; functionTo (listOf package);
       default = _: [];
-      defaultText = literalExpression ''
+      defaultText = lib.literalExpression ''
         python3Packages: with python3Packages; [];
       '';
       description = lib.mdDoc ''
@@ -104,23 +102,23 @@ in {
       '';
     };
 
-    dataDir = mkOption {
-      type = types.str;
+    dataDir = lib.mkOption {
+      type = lib.types.str;
       default = "/var/lib/netbox";
       description = lib.mdDoc ''
         Storage path of netbox.
       '';
     };
 
-    secretKeyFile = mkOption {
-      type = types.path;
+    secretKeyFile = lib.mkOption {
+      type = lib.types.path;
       description = lib.mdDoc ''
         Path to a file containing the secret key.
       '';
     };
 
-    extraConfig = mkOption {
-      type = types.lines;
+    extraConfig = lib.mkOption {
+      type = lib.types.lines;
       default = "";
       description = lib.mdDoc ''
         Additional lines of configuration appended to the `configuration.py`.
@@ -128,8 +126,8 @@ in {
       '';
     };
 
-    enableLdap = mkOption {
-      type = types.bool;
+    enableLdap = lib.mkOption {
+      type = lib.types.bool;
       default = false;
       description = lib.mdDoc ''
         Enable LDAP-Authentication for Netbox.
@@ -138,8 +136,8 @@ in {
       '';
     };
 
-    ldapConfigPath = mkOption {
-      type = types.path;
+    ldapConfigPath = lib.mkOption {
+      type = lib.types.path;
       default = "";
       description = lib.mdDoc ''
         Path to the Configuration-File for LDAP-Authentication, will be loaded as `ldap_config.py`.
@@ -171,17 +169,26 @@ in {
         AUTH_LDAP_FIND_GROUP_PERMS = True
       '';
     };
+    keycloakClientSecret = lib.mkOption {
+      type = with lib.types; nullOr path;
+      default = null;
+      description = lib.mdDoc ''
+        File that contains the keycloak client secret.
+      '';
+    };
   };
 
-  config = mkIf cfg.enable {
+  config = lib.mkIf cfg.enable {
     services.netbox = {
-      plugins = mkIf cfg.enableLdap (ps: [ ps.django-auth-ldap ]);
+      plugins = lib.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";
 
+        GIT_PATH = "${pkgs.gitMinimal}/bin/git";
+
         DATABASE = {
           NAME = "netbox";
           USER = "netbox";
@@ -227,7 +234,10 @@ in {
       extraConfig = ''
         with open("${cfg.secretKeyFile}", "r") as file:
             SECRET_KEY = file.readline()
-      '';
+      '' + (lib.optionalString (cfg.keycloakClientSecret != null) ''
+        with open("${cfg.keycloakClientSecret}", "r") as file:
+            SOCIAL_AUTH_KEYCLOAK_SECRET = file.readline()
+      '');
     };
 
     services.redis.servers.netbox.enable = true;
@@ -264,40 +274,40 @@ in {
         RestartSec = 30;
       };
     in {
-      netbox-migration = {
-        description = "NetBox migrations";
-        wantedBy = [ "netbox.target" ];
-
-        environment = {
-          PYTHONPATH = pkg.pythonPath;
-        };
-
-        serviceConfig = defaultServiceConfig // {
-          Type = "oneshot";
-          ExecStart = ''
-            ${pkg}/bin/netbox migrate
-          '';
-          PrivateTmp = true;
-        };
-      };
-
       netbox = {
         description = "NetBox WSGI Service";
         documentation = [ "https://docs.netbox.dev/" ];
 
         wantedBy = [ "netbox.target" ];
 
-        after = [ "network-online.target" "netbox-migration.service" ];
+        after = [ "network-online.target" ];
         wants = [ "network-online.target" ];
 
+        environment.PYTHONPATH = pkg.pythonPath;
+
         preStart = ''
+          # On the first run, or on upgrade / downgrade, run migrations and related.
+          # This mostly correspond to upstream NetBox's 'upgrade.sh' script.
+          versionFile="${cfg.dataDir}/version"
+
+          if [[ -e "$versionFile" && "$(cat "$versionFile")" == "${cfg.package.version}" ]]; then
+            exit 0
+          fi
+
+          ${pkg}/bin/netbox migrate
           ${pkg}/bin/netbox trace_paths --no-input
           ${pkg}/bin/netbox collectstatic --no-input
           ${pkg}/bin/netbox remove_stale_contenttypes --no-input
+          # TODO: remove the condition when we remove netbox_3_3
+          ${lib.optionalString
+            (lib.versionAtLeast cfg.package.version "3.5.0")
+            "${pkg}/bin/netbox reindex --lazy"}
+          ${pkg}/bin/netbox clearsessions
+          ${pkg}/bin/netbox clearcache
+
+          echo "${cfg.package.version}" > "$versionFile"
         '';
 
-        environment.PYTHONPATH = pkg.pythonPath;
-
         serviceConfig = defaultServiceConfig // {
           ExecStart = ''
             ${pkgs.python3Packages.gunicorn}/bin/gunicorn netbox.wsgi \
@@ -331,7 +341,7 @@ in {
 
         wantedBy = [ "multi-user.target" ];
 
-        after = [ "network-online.target" ];
+        after = [ "network-online.target" "netbox.service" ];
         wants = [ "network-online.target" ];
 
         environment.PYTHONPATH = pkg.pythonPath;
@@ -351,7 +361,7 @@ in {
 
       wantedBy = [ "multi-user.target" ];
 
-      after = [ "network-online.target" ];
+      after = [ "network-online.target" "netbox.service" ];
       wants = [ "network-online.target" ];
 
       timerConfig = {
diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix
index 5cc9ef6dd6d9..cec0b379f67a 100644
--- a/nixos/modules/services/web-servers/caddy/default.nix
+++ b/nixos/modules/services/web-servers/caddy/default.nix
@@ -24,21 +24,26 @@ let
         }
       '';
 
-  configFile =
-    let
-      Caddyfile = pkgs.writeTextDir "Caddyfile" ''
-        {
-          ${cfg.globalConfig}
-        }
-        ${cfg.extraConfig}
-      '';
+  settingsFormat = pkgs.formats.json { };
 
-      Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } ''
-        mkdir -p $out
-        cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile
-        caddy fmt --overwrite $out/Caddyfile
-      '';
-    in
+  configFile =
+    if cfg.settings != { } then
+      settingsFormat.generate "caddy.json" cfg.settings
+    else
+      let
+        Caddyfile = pkgs.writeTextDir "Caddyfile" ''
+          {
+            ${cfg.globalConfig}
+          }
+          ${cfg.extraConfig}
+        '';
+
+        Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } ''
+          mkdir -p $out
+          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";
@@ -299,6 +304,27 @@ in
         which could delay the reload essentially indefinitely.
       '';
     };
+
+    settings = mkOption {
+      type = settingsFormat.type;
+      default = {};
+      description = lib.mdDoc ''
+        Structured configuration for Caddy to generate a Caddy JSON configuration file.
+        See <https://caddyserver.com/docs/json/> for available options.
+
+        ::: {.warning}
+        Using a [Caddyfile](https://caddyserver.com/docs/caddyfile) instead of a JSON config is highly recommended by upstream.
+        There are only very few exception to this.
+
+        Please use a Caddyfile via {option}`services.caddy.configFile`, {option}`services.caddy.virtualHosts` or
+        {option}`services.caddy.extraConfig` with {option}`services.caddy.globalConfig` instead.
+        :::
+
+        ::: {.note}
+        Takes presence over most `services.caddy.*` options, such as {option}`services.caddy.configFile` and {option}`services.caddy.virtualHosts`, if specified.
+        :::
+      '';
+    };
   };
 
   # implementation
diff --git a/nixos/modules/services/x11/desktop-managers/budgie.nix b/nixos/modules/services/x11/desktop-managers/budgie.nix
index bee627ec76c0..a4f8bd5051ec 100644
--- a/nixos/modules/services/x11/desktop-managers/budgie.nix
+++ b/nixos/modules/services/x11/desktop-managers/budgie.nix
@@ -134,6 +134,7 @@ in {
         # Update user directories.
         xdg-user-dirs
       ]
+      ++ lib.optional config.networking.networkmanager.enable pkgs.networkmanagerapplet
       ++ (utils.removePackagesByName [
           cinnamon.nemo
           mate.eom
@@ -192,7 +193,7 @@ in {
     # 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.
+    programs.nm-applet.indicator = true; # Budgie uses 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.
diff --git a/nixos/modules/services/x11/desktop-managers/cinnamon.nix b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
index d2e16da93456..bb42c52b69ca 100644
--- a/nixos/modules/services/x11/desktop-managers/cinnamon.nix
+++ b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
@@ -212,6 +212,13 @@ in
       programs.bash.vteIntegration = mkDefault true;
       programs.zsh.vteIntegration = mkDefault true;
 
+      # Qt application style
+      qt = {
+        enable = mkDefault true;
+        style = mkDefault "gtk2";
+        platformTheme = mkDefault "gtk2";
+      };
+
       # Default Fonts
       fonts.packages = with pkgs; [
         source-code-pro # Default monospace font in 3.32
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index 676d08b93e2c..e6923bcbb56c 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -231,40 +231,14 @@ in
 
     systemd.user.services.dbus.wantedBy = [ "default.target" ];
 
-    programs.dconf.profiles.gdm =
-    let
-      customDconf = pkgs.writeTextFile {
-        name = "gdm-dconf";
-        destination = "/dconf/gdm-custom";
-        text = ''
-          ${optionalString (!cfg.gdm.autoSuspend) ''
-            [org/gnome/settings-daemon/plugins/power]
-            sleep-inactive-ac-type='nothing'
-            sleep-inactive-battery-type='nothing'
-            sleep-inactive-ac-timeout=0
-            sleep-inactive-battery-timeout=0
-          ''}
-        '';
-      };
-
-      customDconfDb = pkgs.stdenv.mkDerivation {
-        name = "gdm-dconf-db";
-        buildCommand = ''
-          ${pkgs.dconf}/bin/dconf compile $out ${customDconf}/dconf
-        '';
+    programs.dconf.profiles.gdm.databases = lib.optionals (!cfg.gdm.autoSuspend) [{
+      settings."org/gnome/settings-daemon/plugins/power" = {
+        sleep-inactive-ac-type = "nothing";
+        sleep-inactive-battery-type = "nothing";
+        sleep-inactive-ac-timeout = lib.gvariant.mkInt32 0;
+        sleep-inactive-battery-timeout = lib.gvariant.mkInt32 0;
       };
-    in pkgs.stdenv.mkDerivation {
-      name = "dconf-gdm-profile";
-      buildCommand = ''
-        # Check that the GDM profile starts with what we expect.
-        if [ $(head -n 1 ${gdm}/share/dconf/profile/gdm) != "user-db:user" ]; then
-          echo "GDM dconf profile changed, please update gdm.nix"
-          exit 1
-        fi
-        # Insert our custom DB behind it.
-        sed '2ifile-db:${customDconfDb}' ${gdm}/share/dconf/profile/gdm > $out
-      '';
-    };
+    }] ++ [ "${gdm}/share/gdm/greeter-dconf-defaults" ];
 
     # Use AutomaticLogin if delay is zero, because it's immediate.
     # Otherwise with TimedLogin with zero seconds the prompt is still
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
index c04edd0d4b7a..47e60236eaeb 100644
--- a/nixos/modules/services/x11/display-managers/sddm.nix
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -267,6 +267,7 @@ in
 
     environment.systemPackages = [ sddm ];
     services.dbus.packages = [ sddm ];
+    systemd.tmpfiles.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 = [
diff --git a/nixos/modules/services/x11/window-managers/default.nix b/nixos/modules/services/x11/window-managers/default.nix
index ce1d4115f225..e180f2693e0c 100644
--- a/nixos/modules/services/x11/window-managers/default.nix
+++ b/nixos/modules/services/x11/window-managers/default.nix
@@ -35,6 +35,7 @@ in
     ./openbox.nix
     ./pekwm.nix
     ./notion.nix
+    ./ragnarwm.nix
     ./ratpoison.nix
     ./sawfish.nix
     ./smallwm.nix
diff --git a/nixos/modules/services/x11/window-managers/dwm.nix b/nixos/modules/services/x11/window-managers/dwm.nix
index e114f2e26b17..82900fd30540 100644
--- a/nixos/modules/services/x11/window-managers/dwm.nix
+++ b/nixos/modules/services/x11/window-managers/dwm.nix
@@ -45,6 +45,7 @@ in
       { name = "dwm";
         start =
           ''
+            export _JAVA_AWT_WM_NONREPARENTING=1
             dwm &
             waitPID=$!
           '';
diff --git a/nixos/modules/services/x11/window-managers/ragnarwm.nix b/nixos/modules/services/x11/window-managers/ragnarwm.nix
new file mode 100644
index 000000000000..0843b872dba5
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/ragnarwm.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.ragnarwm;
+in
+{
+  ###### interface
+
+  options = {
+    services.xserver.windowManager.ragnarwm = {
+      enable = mkEnableOption (lib.mdDoc "ragnarwm");
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ragnarwm;
+        defaultText = literalExpression "pkgs.ragnarwm";
+        description = lib.mdDoc ''
+          The ragnar package to use.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    services.xserver.displayManager.sessionPackages = [ cfg.package ];
+    environment.systemPackages = [ cfg.package ];
+  };
+
+  meta.maintainers = with lib.maintainers; [ sigmanificient ];
+}
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index 04d90968c4c1..8bd450d7343b 100755
--- a/nixos/modules/system/activation/switch-to-configuration.pl
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -313,7 +313,8 @@ sub unrecord_unit {
 # needs to be restarted or reloaded. If the units differ, the service
 # is restarted unless the only difference is `X-Reload-Triggers` in the
 # `Unit` section. If this is the only modification, the unit is reloaded
-# instead of restarted.
+# instead of restarted. If the only difference is `Options` in the
+# `[Mount]` section, the unit is reloaded rather than restarted.
 # Returns:
 # - 0 if the units are equal
 # - 1 if the units are different and a restart action is required
@@ -390,6 +391,11 @@ sub compare_units { ## no critic(Subroutines::ProhibitExcessComplexity)
                         next;
                     }
                 }
+                # If this is a mount unit, check if it was only `Options`
+                if ($section_name eq "Mount" and $ini_key eq "Options") {
+                    $ret = 2;
+                    next;
+                }
                 return 1;
             }
         }
@@ -440,10 +446,18 @@ sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutin
         # properties (resource limits and inotify watches)
         # seem to get applied on daemon-reload.
     } elsif ($unit =~ /\.mount$/msx) {
-        # Reload the changed mount unit to force a remount.
-        # FIXME: only reload when Options= changed, restart otherwise
-        $units_to_reload->{$unit} = 1;
-        record_unit($reload_list_file, $unit);
+        # Just restart the unit. We wouldn't have gotten into this subroutine
+        # if only `Options` was changed, in which case the unit would be reloaded.
+        # The only exception is / and /nix because it's very unlikely we can safely
+        # unmount them so we reload them instead. This means that we may not get
+        # all changes into the running system but it's better than crashing it.
+        if ($unit eq "-.mount" or $unit eq "nix.mount") {
+            $units_to_reload->{$unit} = 1;
+            record_unit($reload_list_file, $unit);
+        } else {
+            $units_to_restart->{$unit} = 1;
+            record_unit($restart_list_file, $unit);
+        }
     } elsif ($unit =~ /\.socket$/msx) {
         # FIXME: do something?
         # Attempt to fix this: https://github.com/NixOS/nixpkgs/pull/141192
diff --git a/nixos/modules/system/boot/binfmt.nix b/nixos/modules/system/boot/binfmt.nix
index bf1688feb19e..fb7afce4a580 100644
--- a/nixos/modules/system/boot/binfmt.nix
+++ b/nixos/modules/system/boot/binfmt.nix
@@ -102,20 +102,28 @@ let
       mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\x00'';
     };
     mips-linux = {
-      magicOrExtension = ''\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08'';
-      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
+      magicOrExtension = ''\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20'';
     };
     mipsel-linux = {
-      magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00'';
-      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+      magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00'';
     };
     mips64-linux = {
       magicOrExtension = ''\x7fELF\x02\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08'';
-      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff'';
     };
     mips64el-linux = {
       magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00'';
-      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+    };
+    mips64-linuxabin32 = {
+      magicOrExtension = ''\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20'';
+    };
+    mips64el-linuxabin32 = {
+      magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00'';
     };
     riscv32-linux = {
       magicOrExtension = ''\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00'';
@@ -137,14 +145,8 @@ let
       magicOrExtension = ''\x00asm'';
       mask = ''\xff\xff\xff\xff'';
     };
-    x86_64-windows = {
-      magicOrExtension = "exe";
-      recognitionType = "extension";
-    };
-    i686-windows = {
-      magicOrExtension = "exe";
-      recognitionType = "extension";
-    };
+    x86_64-windows.magicOrExtension = "MZ";
+    i686-windows.magicOrExtension = "MZ";
   };
 
 in {
diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl
index a84e374624d1..d1e7a0cb8178 100644
--- a/nixos/modules/system/boot/loader/grub/install-grub.pl
+++ b/nixos/modules/system/boot/loader/grub/install-grub.pl
@@ -516,38 +516,53 @@ sub addEntry {
     $conf .= "}\n\n";
 }
 
+sub addGeneration {
+    my ($name, $nameSuffix, $path, $options, $current) = @_;
 
-# Add default entries.
-$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
+    # Do not search for grand children
+    my @links = sort (glob "$path/specialisation/*");
 
-addEntry("@distroName@ - Default", $defaultConfig, $entryOptions, 1);
+    if ($current != 1 && scalar(@links) != 0) {
+        $conf .= "submenu \"> $name$nameSuffix\" --class submenu {\n";
+    }
 
-$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
+    addEntry("$name" . (scalar(@links) == 0 ? "" : " - Default") . $nameSuffix, $path, $options, $current);
 
-# Find all the children of the current default configuration
-# Do not search for grand children
-my @links = sort (glob "$defaultConfig/specialisation/*");
-foreach my $link (@links) {
+    # Find all the children of the current default configuration
+    # Do not search for grand children
+    foreach my $link (@links) {
 
-    my $entryName = "";
+        my $entryName = "";
 
-    my $cfgName = readFile("$link/configuration-name");
+        my $cfgName = readFile("$link/configuration-name");
 
-    my $date = strftime("%F", localtime(lstat($link)->mtime));
-    my $version =
-        -e "$link/nixos-version"
-        ? readFile("$link/nixos-version")
-        : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
+        my $date = strftime("%F", localtime(lstat($link)->mtime));
+        my $version =
+            -e "$link/nixos-version"
+            ? readFile("$link/nixos-version")
+            : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
 
-    if ($cfgName) {
-        $entryName = $cfgName;
-    } else {
-        my $linkname = basename($link);
-        $entryName = "($linkname - $date - $version)";
+        if ($cfgName) {
+            $entryName = $cfgName;
+        } else {
+            my $linkname = basename($link);
+            $entryName = "($linkname - $date - $version)";
+        }
+        addEntry("$name - $entryName", $link, "", 1);
+    }
+
+    if ($current != 1 && scalar(@links) != 0) {
+        $conf .= "}\n";
     }
-    addEntry("@distroName@ - $entryName", $link, "", 1);
 }
 
+# Add default entries.
+$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
+
+addGeneration("@distroName@", "", $defaultConfig, $entryOptions, 1);
+
+$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
+
 my $grubBootPath = $grubBoot->path;
 # extraEntries could refer to @bootRoot@, which we have to substitute
 $conf =~ s/\@bootRoot\@/$grubBootPath/g;
@@ -577,7 +592,7 @@ sub addProfile {
             -e "$link/nixos-version"
             ? readFile("$link/nixos-version")
             : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]);
-        addEntry("@distroName@ - Configuration " . nrFromGen($link) . " ($date - $version)", $link, $subEntryOptions, 0);
+        addGeneration("@distroName@ - Configuration " . nrFromGen($link), " ($date - $version)", $link, $subEntryOptions, 0);
     }
 
     $conf .= "}\n";
diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh
index f9a2084ea9e8..c0af29e3b990 100755
--- a/nixos/modules/system/boot/stage-2-init.sh
+++ b/nixos/modules/system/boot/stage-2-init.sh
@@ -104,7 +104,10 @@ fi
 
 
 # Required by the activation script
-install -m 0755 -d /etc /etc/nixos
+install -m 0755 -d /etc
+if [ -d "/etc/nixos" ]; then
+    install -m 0755 -d /etc/nixos
+fi
 install -m 01777 -d /tmp
 
 
diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix
index 3f40a5b2dfa0..1b9584cb57bb 100644
--- a/nixos/modules/system/boot/systemd/initrd.nix
+++ b/nixos/modules/system/boot/systemd/initrd.nix
@@ -333,6 +333,14 @@ in {
       visible = "shallow";
       description = lib.mdDoc "Definition of slice configurations.";
     };
+
+    enableTpm2 = mkOption {
+      default = true;
+      type = types.bool;
+      description = lib.mdDoc ''
+        Whether to enable TPM2 support in the initrd.
+      '';
+    };
   };
 
   config = mkIf (config.boot.initrd.enable && cfg.enable) {
@@ -342,8 +350,8 @@ in {
       # systemd needs this for some features
       "autofs4"
       # systemd-cryptenroll
-      "tpm-tis"
-    ] ++ lib.optional (pkgs.stdenv.hostPlatform.system != "riscv64-linux") "tpm-crb";
+    ] ++ lib.optional cfg.enableTpm2 "tpm-tis"
+    ++ lib.optional (cfg.enableTpm2 && pkgs.stdenv.hostPlatform.system != "riscv64-linux") "tpm-crb";
 
     boot.initrd.systemd = {
       initrdBin = [pkgs.bash pkgs.coreutils cfg.package.kmod cfg.package] ++ config.system.fsPackages;
@@ -421,11 +429,11 @@ in {
 
         # so NSS can look up usernames
         "${pkgs.glibc}/lib/libnss_files.so.2"
-      ] ++ optionals cfg.package.withCryptsetup [
+      ] ++ optionals (cfg.package.withCryptsetup && cfg.enableTpm2) [
         # tpm2 support
         "${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-tpm2.so"
         pkgs.tpm2-tss
-
+      ] ++ optionals cfg.package.withCryptsetup [
         # fido2 support
         "${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-fido2.so"
         "${pkgs.libfido2}/lib/libfido2.so.1"
diff --git a/nixos/modules/system/boot/systemd/user.nix b/nixos/modules/system/boot/systemd/user.nix
index 1b6398d2f929..64dc19633eca 100644
--- a/nixos/modules/system/boot/systemd/user.nix
+++ b/nixos/modules/system/boot/systemd/user.nix
@@ -230,5 +230,9 @@ in {
           });
         })
         cfg.tmpfiles.users;
+
+    system.userActivationScripts.tmpfiles = ''
+      ${config.systemd.package}/bin/systemd-tmpfiles --user --create --remove
+    '';
   };
 }
diff --git a/nixos/modules/virtualisation/anbox.nix b/nixos/modules/virtualisation/anbox.nix
index c7e9e23c4c92..523d9a9576ef 100644
--- a/nixos/modules/virtualisation/anbox.nix
+++ b/nixos/modules/virtualisation/anbox.nix
@@ -5,7 +5,7 @@ with lib;
 let
 
   cfg = config.virtualisation.anbox;
-  kernelPackages = config.boot.kernelPackages;
+
   addrOpts = v: addr: pref: name: {
     address = mkOption {
       default = addr;
@@ -25,6 +25,28 @@ let
     };
   };
 
+  finalImage = if cfg.imageModifications == "" then cfg.image else ( pkgs.callPackage (
+    { runCommandNoCC, squashfsTools }:
+
+    runCommandNoCC "${cfg.image.name}-modified.img" {
+      nativeBuildInputs = [
+        squashfsTools
+      ];
+    } ''
+      echo "-> Extracting Anbox root image..."
+      unsquashfs -dest rootfs ${cfg.image}
+
+      echo "-> Modifying Anbox root image..."
+      (
+      cd rootfs
+      ${cfg.imageModifications}
+      )
+
+      echo "-> Packing modified Anbox root image..."
+      mksquashfs rootfs $out -comp xz -no-xattrs -all-root
+    ''
+  ) { });
+
 in
 
 {
@@ -42,6 +64,18 @@ in
       '';
     };
 
+    imageModifications = mkOption {
+      default = "";
+      type = types.lines;
+      description = lib.mdDoc ''
+        Commands to edit the image filesystem.
+
+        This can be used to e.g. bundle a privileged F-Droid.
+
+        Commands are ran with PWD being at the root of the filesystem.
+      '';
+    };
+
     extraInit = mkOption {
       type = types.lines;
       default = "";
@@ -67,16 +101,19 @@ in
   config = mkIf cfg.enable {
 
     assertions = singleton {
-      assertion = versionAtLeast (getVersion config.boot.kernelPackages.kernel) "4.18";
-      message = "Anbox needs user namespace support to work properly";
+      assertion = with config.boot.kernelPackages; kernelAtLeast "5.5" && kernelOlder "5.18";
+      message = "Anbox needs a kernel with binder and ashmem support";
     };
 
     environment.systemPackages = with pkgs; [ anbox ];
 
-    services.udev.extraRules = ''
-      KERNEL=="ashmem", NAME="%k", MODE="0666"
-      KERNEL=="binder*", NAME="%k", MODE="0666"
-    '';
+    systemd.mounts = singleton {
+      requiredBy = [ "anbox-container-manager.service" ];
+      description = "Anbox Binder File System";
+      what = "binder";
+      where = "/dev/binderfs";
+      type = "binder";
+    };
 
     virtualisation.lxc.enable = true;
     networking.bridges.anbox0.interfaces = [];
@@ -87,6 +124,9 @@ in
       internalInterfaces = [ "anbox0" ];
     };
 
+    # Ensures NetworkManager doesn't touch anbox0
+    networking.networkmanager.unmanaged = [ "anbox0" ];
+
     systemd.services.anbox-container-manager = let
       anboxloc = "/var/lib/anbox";
     in {
@@ -121,12 +161,13 @@ in
         ExecStart = ''
           ${pkgs.anbox}/bin/anbox container-manager \
             --data-path=${anboxloc} \
-            --android-image=${cfg.image} \
+            --android-image=${finalImage} \
             --container-network-address=${cfg.ipv4.container.address} \
             --container-network-gateway=${cfg.ipv4.gateway.address} \
             --container-network-dns-servers=${cfg.ipv4.dns} \
             --use-rootfs-overlay \
-            --privileged
+            --privileged \
+            --daemon
         '';
       };
     };
diff --git a/nixos/modules/virtualisation/docker.nix b/nixos/modules/virtualisation/docker.nix
index 20f47a76c87b..6fe460316091 100644
--- a/nixos/modules/virtualisation/docker.nix
+++ b/nixos/modules/virtualisation/docker.nix
@@ -236,8 +236,8 @@ in
       };
 
       assertions = [
-        { assertion = cfg.enableNvidia -> config.hardware.opengl.driSupport32Bit or false;
-          message = "Option enableNvidia requires 32bit support libraries";
+        { assertion = cfg.enableNvidia && pkgs.stdenv.isx86_64 -> config.hardware.opengl.driSupport32Bit or false;
+          message = "Option enableNvidia on x86_64 requires 32bit support libraries";
         }];
 
       virtualisation.docker.daemon.settings = {
diff --git a/nixos/modules/virtualisation/lxc-container.nix b/nixos/modules/virtualisation/lxc-container.nix
index 55b285b69147..9402d3bf37d0 100644
--- a/nixos/modules/virtualisation/lxc-container.nix
+++ b/nixos/modules/virtualisation/lxc-container.nix
@@ -1,96 +1,16 @@
 { lib, config, pkgs, ... }:
 
-with lib;
-
 let
-  templateSubmodule = { ... }: {
-    options = {
-      enable = mkEnableOption (lib.mdDoc "this template");
-
-      target = mkOption {
-        description = lib.mdDoc "Path in the container";
-        type = types.path;
-      };
-      template = mkOption {
-        description = lib.mdDoc ".tpl file for rendering the target";
-        type = types.path;
-      };
-      when = mkOption {
-        description = lib.mdDoc "Events which trigger a rewrite (create, copy)";
-        type = types.listOf (types.str);
-      };
-      properties = mkOption {
-        description = lib.mdDoc "Additional properties";
-        type = types.attrs;
-        default = {};
-      };
-    };
-  };
-
-  toYAML = name: data: pkgs.writeText name (generators.toYAML {} data);
-
   cfg = config.virtualisation.lxc;
-  templates = if cfg.templates != {} then let
-    list = mapAttrsToList (name: value: { inherit name; } // value)
-      (filterAttrs (name: value: value.enable) cfg.templates);
-  in
-    {
-      files = map (tpl: {
-        source = tpl.template;
-        target = "/templates/${tpl.name}.tpl";
-      }) list;
-      properties = listToAttrs (map (tpl: nameValuePair tpl.target {
-        when = tpl.when;
-        template = "${tpl.name}.tpl";
-        properties = tpl.properties;
-      }) list);
-    }
-  else { files = []; properties = {}; };
-
-in
-{
+in {
   imports = [
-    ../installer/cd-dvd/channel.nix
-    ../profiles/clone-config.nix
-    ../profiles/minimal.nix
+    ./lxc-instance-common.nix
   ];
 
   options = {
     virtualisation.lxc = {
-      templates = mkOption {
-        description = lib.mdDoc "Templates for LXD";
-        type = types.attrsOf (types.submodule (templateSubmodule));
-        default = {};
-        example = literalExpression ''
-          {
-            # create /etc/hostname on container creation. also requires networking.hostName = "" to be set
-            "hostname" = {
-              enable = true;
-              target = "/etc/hostname";
-              template = builtins.toFile "hostname.tpl" "{{ container.name }}";
-              when = [ "create" ];
-            };
-            # create /etc/nixos/hostname.nix with a configuration for keeping the hostname applied
-            "hostname-nix" = {
-              enable = true;
-              target = "/etc/nixos/hostname.nix";
-              template = builtins.toFile "hostname-nix.tpl" "{ ... }: { networking.hostName = \"{{ container.name }}\"; }";
-              # copy keeps the file updated when the container is changed
-              when = [ "create" "copy" ];
-            };
-            # copy allow the user to specify a custom configuration.nix
-            "configuration-nix" = {
-              enable = true;
-              target = "/etc/nixos/configuration.nix";
-              template = builtins.toFile "configuration-nix" "{{ config_get(\"user.user-data\", properties.default) }}";
-              when = [ "create" ];
-            };
-          };
-        '';
-      };
-
-      privilegedContainer = mkOption {
-        type = types.bool;
+      privilegedContainer = lib.mkOption {
+        type = lib.types.bool;
         default = false;
         description = lib.mdDoc ''
           Whether this LXC container will be running as a privileged container or not. If set to `true` then
@@ -116,24 +36,6 @@ in
         ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
       '';
 
-    system.build.metadata = pkgs.callPackage ../../lib/make-system-tarball.nix {
-      contents = [
-        {
-          source = toYAML "metadata.yaml" {
-            architecture = builtins.elemAt (builtins.match "^([a-z0-9_]+).+" (toString pkgs.system)) 0;
-            creation_date = 1;
-            properties = {
-              description = "${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;
-          };
-          target = "/metadata.yaml";
-        }
-      ] ++ templates.files;
-    };
-
     # TODO: build rootfs as squashfs for faster unpack
     system.build.tarball = pkgs.callPackage ../../lib/make-system-tarball.nix {
       extraArgs = "--owner=0";
@@ -180,7 +82,7 @@ in
           ProtectKernelTunables=no
           NoNewPrivileges=no
           LoadCredential=
-        '' + optionalString cfg.privilegedContainer ''
+        '' + lib.optionalString cfg.privilegedContainer ''
           # Additional settings for privileged containers
           ProtectHome=no
           ProtectSystem=no
@@ -193,28 +95,8 @@ in
       })
     ];
 
-    # Allow the user to login as root without password.
-    users.users.root.initialHashedPassword = mkOverride 150 "";
-
-    system.activationScripts.installInitScript = mkForce ''
+    system.activationScripts.installInitScript = lib.mkForce ''
       ln -fs $systemConfig/init /sbin/init
     '';
-
-    # Some more help text.
-    services.getty.helpLine =
-      ''
-
-        Log in as "root" with an empty password.
-      '';
-
-    # Containers should be light-weight, so start sshd on demand.
-    services.openssh.enable = mkDefault true;
-    services.openssh.startWhenNeeded = mkDefault true;
-
-    # As this is intended as a standalone image, undo some of the minimal profile stuff
-    environment.noXlibs = false;
-    documentation.enable = true;
-    documentation.nixos.enable = true;
-    services.logrotate.enable = true;
   };
 }
diff --git a/nixos/modules/virtualisation/lxc-image-metadata.nix b/nixos/modules/virtualisation/lxc-image-metadata.nix
new file mode 100644
index 000000000000..2c0568b4c468
--- /dev/null
+++ b/nixos/modules/virtualisation/lxc-image-metadata.nix
@@ -0,0 +1,104 @@
+{ lib, config, pkgs, ... }:
+
+let
+  templateSubmodule = {...}: {
+    options = {
+      enable = lib.mkEnableOption "this template";
+
+      target = lib.mkOption {
+        description = "Path in the container";
+        type = lib.types.path;
+      };
+      template = lib.mkOption {
+        description = ".tpl file for rendering the target";
+        type = lib.types.path;
+      };
+      when = lib.mkOption {
+        description = "Events which trigger a rewrite (create, copy)";
+        type = lib.types.listOf (lib.types.str);
+      };
+      properties = lib.mkOption {
+        description = "Additional properties";
+        type = lib.types.attrs;
+        default = {};
+      };
+    };
+  };
+
+  toYAML = name: data: pkgs.writeText name (lib.generators.toYAML {} data);
+
+  cfg = config.virtualisation.lxc;
+  templates = if cfg.templates != {} then let
+    list = lib.mapAttrsToList (name: value: { inherit name; } // value)
+      (lib.filterAttrs (name: value: value.enable) cfg.templates);
+  in
+    {
+      files = map (tpl: {
+        source = tpl.template;
+        target = "/templates/${tpl.name}.tpl";
+      }) list;
+      properties = lib.listToAttrs (map (tpl: lib.nameValuePair tpl.target {
+        when = tpl.when;
+        template = "${tpl.name}.tpl";
+        properties = tpl.properties;
+      }) list);
+    }
+  else { files = []; properties = {}; };
+
+in {
+  options = {
+    virtualisation.lxc = {
+      templates = lib.mkOption {
+        description = "Templates for LXD";
+        type = lib.types.attrsOf (lib.types.submodule templateSubmodule);
+        default = {};
+        example = lib.literalExpression ''
+          {
+            # create /etc/hostname on container creation
+            "hostname" = {
+              enable = true;
+              target = "/etc/hostname";
+              template = builtins.writeFile "hostname.tpl" "{{ container.name }}";
+              when = [ "create" ];
+            };
+            # create /etc/nixos/hostname.nix with a configuration for keeping the hostname applied
+            "hostname-nix" = {
+              enable = true;
+              target = "/etc/nixos/hostname.nix";
+              template = builtins.writeFile "hostname-nix.tpl" "{ ... }: { networking.hostName = "{{ container.name }}"; }";
+              # copy keeps the file updated when the container is changed
+              when = [ "create" "copy" ];
+            };
+            # copy allow the user to specify a custom configuration.nix
+            "configuration-nix" = {
+              enable = true;
+              target = "/etc/nixos/configuration.nix";
+              template = builtins.writeFile "configuration-nix" "{{ config_get(\"user.user-data\", properties.default) }}";
+              when = [ "create" ];
+            };
+          };
+        '';
+      };
+    };
+  };
+
+  config = {
+    system.build.metadata = pkgs.callPackage ../../lib/make-system-tarball.nix {
+      contents = [
+        {
+          source = toYAML "metadata.yaml" {
+            architecture = builtins.elemAt (builtins.match "^([a-z0-9_]+).+" (toString pkgs.system)) 0;
+            creation_date = 1;
+            properties = {
+              description = "${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;
+          };
+          target = "/metadata.yaml";
+        }
+      ] ++ templates.files;
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/lxc-instance-common.nix b/nixos/modules/virtualisation/lxc-instance-common.nix
new file mode 100644
index 000000000000..d6a0e05fb1c9
--- /dev/null
+++ b/nixos/modules/virtualisation/lxc-instance-common.nix
@@ -0,0 +1,30 @@
+{lib, ...}:
+
+{
+  imports = [
+    ./lxc-image-metadata.nix
+
+    ../installer/cd-dvd/channel.nix
+    ../profiles/clone-config.nix
+    ../profiles/minimal.nix
+  ];
+
+  # Allow the user to login as root without password.
+  users.users.root.initialHashedPassword = lib.mkOverride 150 "";
+
+  # Some more help text.
+  services.getty.helpLine = ''
+
+    Log in as "root" with an empty password.
+  '';
+
+  # Containers should be light-weight, so start sshd on demand.
+  services.openssh.enable = lib.mkDefault true;
+  services.openssh.startWhenNeeded = lib.mkDefault true;
+
+  # As this is intended as a standalone image, undo some of the minimal profile stuff
+  environment.noXlibs = false;
+  documentation.enable = true;
+  documentation.nixos.enable = true;
+  services.logrotate.enable = true;
+}
diff --git a/nixos/modules/virtualisation/lxd-virtual-machine.nix b/nixos/modules/virtualisation/lxd-virtual-machine.nix
new file mode 100644
index 000000000000..ba729465ec2f
--- /dev/null
+++ b/nixos/modules/virtualisation/lxd-virtual-machine.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, ... }:
+
+let
+  serialDevice =
+    if pkgs.stdenv.hostPlatform.isx86
+    then "ttyS0"
+    else "ttyAMA0"; # aarch64
+in {
+  imports = [
+    ./lxc-instance-common.nix
+
+    ../profiles/qemu-guest.nix
+  ];
+
+  config = {
+    system.build.qemuImage = import ../../lib/make-disk-image.nix {
+      inherit pkgs lib config;
+
+      partitionTableType = "efi";
+      format = "qcow2-compressed";
+      copyChannel = true;
+    };
+
+    fileSystems = {
+      "/" = {
+        device = "/dev/disk/by-label/nixos";
+        autoResize = true;
+        fsType = "ext4";
+      };
+      "/boot" = {
+        device = "/dev/disk/by-label/ESP";
+        fsType = "vfat";
+      };
+    };
+
+    boot.growPartition = true;
+    boot.loader.systemd-boot.enable = true;
+
+    # image building needs to know what device to install bootloader on
+    boot.loader.grub.device = "/dev/vda";
+
+    boot.kernelParams = ["console=tty1" "console=${serialDevice}"];
+
+    virtualisation.lxd.agent.enable = lib.mkDefault true;
+  };
+}
diff --git a/nixos/modules/virtualisation/lxd.nix b/nixos/modules/virtualisation/lxd.nix
index e22ba9a0ae2c..e30fbebb662c 100644
--- a/nixos/modules/virtualisation/lxd.nix
+++ b/nixos/modules/virtualisation/lxd.nix
@@ -2,21 +2,20 @@
 
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   cfg = config.virtualisation.lxd;
+  preseedFormat = pkgs.formats.yaml {};
 in {
   imports = [
-    (mkRemovedOptionModule [ "virtualisation" "lxd" "zfsPackage" ] "Override zfs in an overlay instead to override it globally")
+    (lib.mkRemovedOptionModule [ "virtualisation" "lxd" "zfsPackage" ] "Override zfs in an overlay instead to override it globally")
   ];
 
   ###### interface
 
   options = {
     virtualisation.lxd = {
-      enable = mkOption {
-        type = types.bool;
+      enable = lib.mkOption {
+        type = lib.types.bool;
         default = false;
         description = lib.mdDoc ''
           This option enables lxd, a daemon that manages
@@ -32,28 +31,28 @@ in {
         '';
       };
 
-      package = mkOption {
-        type = types.package;
+      package = lib.mkOption {
+        type = lib.types.package;
         default = pkgs.lxd;
-        defaultText = literalExpression "pkgs.lxd";
+        defaultText = lib.literalExpression "pkgs.lxd";
         description = lib.mdDoc ''
           The LXD package to use.
         '';
       };
 
-      lxcPackage = mkOption {
-        type = types.package;
+      lxcPackage = lib.mkOption {
+        type = lib.types.package;
         default = pkgs.lxc;
-        defaultText = literalExpression "pkgs.lxc";
+        defaultText = lib.literalExpression "pkgs.lxc";
         description = lib.mdDoc ''
           The LXC package to use with LXD (required for AppArmor profiles).
         '';
       };
 
-      zfsSupport = mkOption {
-        type = types.bool;
+      zfsSupport = lib.mkOption {
+        type = lib.types.bool;
         default = config.boot.zfs.enabled;
-        defaultText = literalExpression "config.boot.zfs.enabled";
+        defaultText = lib.literalExpression "config.boot.zfs.enabled";
         description = lib.mdDoc ''
           Enables lxd to use zfs as a storage for containers.
 
@@ -62,8 +61,8 @@ in {
         '';
       };
 
-      recommendedSysctlSettings = mkOption {
-        type = types.bool;
+      recommendedSysctlSettings = lib.mkOption {
+        type = lib.types.bool;
         default = false;
         description = lib.mdDoc ''
           Enables various settings to avoid common pitfalls when
@@ -75,8 +74,67 @@ in {
         '';
       };
 
-      startTimeout = mkOption {
-        type = types.int;
+      preseed = lib.mkOption {
+        type = lib.types.nullOr (lib.types.submodule {
+          freeformType = preseedFormat.type;
+        });
+
+        default = null;
+
+        description = lib.mdDoc ''
+          Configuration for LXD preseed, see
+          <https://documentation.ubuntu.com/lxd/en/latest/howto/initialize/#initialize-preseed>
+          for supported values.
+
+          Changes to this will be re-applied to LXD which will overwrite existing entities or create missing ones,
+          but entities will *not* be removed by preseed.
+        '';
+
+        example = lib.literalExpression ''
+          {
+            networks = [
+              {
+                name = "lxdbr0";
+                type = "bridge";
+                config = {
+                  "ipv4.address" = "10.0.100.1/24";
+                  "ipv4.nat" = "true";
+                };
+              }
+            ];
+            profiles = [
+              {
+                name = "default";
+                devices = {
+                  eth0 = {
+                    name = "eth0";
+                    network = "lxdbr0";
+                    type = "nic";
+                  };
+                  root = {
+                    path = "/";
+                    pool = "default";
+                    size = "35GiB";
+                    type = "disk";
+                  };
+                };
+              }
+            ];
+            storage_pools = [
+              {
+                name = "default";
+                driver = "dir";
+                config = {
+                  source = "/var/lib/lxd/storage-pools/default";
+                };
+              }
+            ];
+          }
+        '';
+      };
+
+      startTimeout = lib.mkOption {
+        type = lib.types.int;
         default = 600;
         apply = toString;
         description = lib.mdDoc ''
@@ -91,13 +149,13 @@ in {
           Enables the (experimental) LXD UI.
         '');
 
-        package = mkPackageOption pkgs.lxd-unwrapped "ui" { };
+        package = lib.mkPackageOption pkgs.lxd-unwrapped "ui" { };
       };
     };
   };
 
   ###### implementation
-  config = mkIf cfg.enable {
+  config = lib.mkIf cfg.enable {
     environment.systemPackages = [ cfg.package ];
 
     # Note: the following options are also declared in virtualisation.lxc, but
@@ -139,19 +197,19 @@ in {
       wantedBy = [ "multi-user.target" ];
       after = [
         "network-online.target"
-        (mkIf config.virtualisation.lxc.lxcfs.enable "lxcfs.service")
+        (lib.mkIf config.virtualisation.lxc.lxcfs.enable "lxcfs.service")
       ];
       requires = [
         "network-online.target"
         "lxd.socket"
-        (mkIf config.virtualisation.lxc.lxcfs.enable "lxcfs.service")
+        (lib.mkIf config.virtualisation.lxc.lxcfs.enable "lxcfs.service")
       ];
       documentation = [ "man:lxd(1)" ];
 
       path = [ pkgs.util-linux ]
-        ++ optional cfg.zfsSupport config.boot.zfs.package;
+        ++ lib.optional cfg.zfsSupport config.boot.zfs.package;
 
-      environment = mkIf (cfg.ui.enable) {
+      environment = lib.mkIf (cfg.ui.enable) {
         "LXD_UI" = cfg.ui.package;
       };
 
@@ -173,11 +231,26 @@ in {
         # By default, `lxd` loads configuration files from hard-coded
         # `/usr/share/lxc/config` - since this is a no-go for us, we have to
         # explicitly tell it where the actual configuration files are
-        Environment = mkIf (config.virtualisation.lxc.lxcfs.enable)
+        Environment = lib.mkIf (config.virtualisation.lxc.lxcfs.enable)
           "LXD_LXC_TEMPLATE_CONFIG=${pkgs.lxcfs}/share/lxc/config";
       };
     };
 
+    systemd.services.lxd-preseed = lib.mkIf (cfg.preseed != null) {
+      description = "LXD initialization with preseed file";
+      wantedBy = ["multi-user.target"];
+      requires = ["lxd.service"];
+      after = ["lxd.service"];
+
+      script = ''
+        ${pkgs.coreutils}/bin/cat ${preseedFormat.generate "lxd-preseed.yaml" cfg.preseed} | ${cfg.package}/bin/lxd init --preseed
+      '';
+
+      serviceConfig = {
+        Type = "oneshot";
+      };
+    };
+
     users.groups.lxd = {};
 
     users.users.root = {
@@ -185,7 +258,7 @@ in {
       subGidRanges = [ { startGid = 1000000; count = 65536; } ];
     };
 
-    boot.kernel.sysctl = mkIf cfg.recommendedSysctlSettings {
+    boot.kernel.sysctl = lib.mkIf cfg.recommendedSysctlSettings {
       "fs.inotify.max_queued_events" = 1048576;
       "fs.inotify.max_user_instances" = 1048576;
       "fs.inotify.max_user_watches" = 1048576;
@@ -196,7 +269,7 @@ in {
       "kernel.keys.maxkeys" = 2000;
     };
 
-    boot.kernelModules = [ "veth" "xt_comment" "xt_CHECKSUM" "xt_MASQUERADE" ]
-      ++ optionals (!config.networking.nftables.enable) [ "iptable_mangle" ];
+    boot.kernelModules = [ "veth" "xt_comment" "xt_CHECKSUM" "xt_MASQUERADE" "vhost_vsock" ]
+      ++ lib.optionals (!config.networking.nftables.enable) [ "iptable_mangle" ];
   };
 }
diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix
index 125086294d41..29dcdab7d18e 100644
--- a/nixos/release-combined.nix
+++ b/nixos/release-combined.nix
@@ -158,6 +158,11 @@ in rec {
         (onFullSupported "nixpkgs.emacs")
         (onFullSupported "nixpkgs.jdk")
         ["nixpkgs.tarball"]
+
+        # Ensure that nixpkgs-check-by-name is available in all release channels and nixos-unstable,
+        # so that a pre-built version can be used in CI for PR's on the corresponding development branches.
+        # See ../pkgs/test/nixpkgs-check-by-name/README.md
+        (onSystems ["x86_64-linux"] "nixpkgs.tests.nixpkgs-check-by-name")
       ];
     };
 }
diff --git a/nixos/release.nix b/nixos/release.nix
index 6da6faab73be..abaa7ef9a711 100644
--- a/nixos/release.nix
+++ b/nixos/release.nix
@@ -44,10 +44,13 @@ let
   pkgs = import ./.. { system = "x86_64-linux"; };
 
 
-  versionModule =
-    { system.nixos.versionSuffix = versionSuffix;
-      system.nixos.revision = nixpkgs.rev or nixpkgs.shortRev;
-    };
+  versionModule = { config, ... }: {
+    system.nixos.versionSuffix = versionSuffix;
+    system.nixos.revision = nixpkgs.rev or nixpkgs.shortRev;
+
+    # At creation time we do not have state yet, so just default to latest.
+    system.stateVersion = config.system.nixos.version;
+  };
 
   makeModules = module: rest: [ configuration versionModule module rest ];
 
@@ -310,7 +313,7 @@ in rec {
   );
 
   # An image that can be imported into lxd and used for container creation
-  lxdImage = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system:
+  lxdContainerImage = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system:
 
     with import ./.. { inherit system; };
 
@@ -319,14 +322,46 @@ in rec {
       modules =
         [ configuration
           versionModule
-          ./maintainers/scripts/lxd/lxd-image.nix
+          ./maintainers/scripts/lxd/lxd-container-image.nix
         ];
     }).config.system.build.tarball)
 
   );
 
   # Metadata for the lxd image
-  lxdMeta = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system:
+  lxdContainerMeta = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system:
+
+    with import ./.. { inherit system; };
+
+    hydraJob ((import lib/eval-config.nix {
+      inherit system;
+      modules =
+        [ configuration
+          versionModule
+          ./maintainers/scripts/lxd/lxd-container-image.nix
+        ];
+    }).config.system.build.metadata)
+
+  );
+
+  # An image that can be imported into lxd and used for container creation
+  lxdVirtualMachineImage = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system:
+
+    with import ./.. { inherit system; };
+
+    hydraJob ((import lib/eval-config.nix {
+      inherit system;
+      modules =
+        [ configuration
+          versionModule
+          ./maintainers/scripts/lxd/lxd-virtual-machine-image.nix
+        ];
+    }).config.system.build.qemuImage)
+
+  );
+
+  # Metadata for the lxd image
+  lxdVirtualMachineImageMeta = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system:
 
     with import ./.. { inherit system; };
 
@@ -335,7 +370,7 @@ in rec {
       modules =
         [ configuration
           versionModule
-          ./maintainers/scripts/lxd/lxd-image.nix
+          ./maintainers/scripts/lxd/lxd-virtual-machine-image.nix
         ];
     }).config.system.build.metadata)
 
diff --git a/nixos/tests/akkoma.nix b/nixos/tests/akkoma.nix
index 7115c0beed34..287e2d485999 100644
--- a/nixos/tests/akkoma.nix
+++ b/nixos/tests/akkoma.nix
@@ -33,7 +33,10 @@ let
 
     echo '${userPassword}' | ${pkgs.toot}/bin/toot login_cli -i "akkoma.nixos.test" -e "jamy@nixos.test"
     echo "y" | ${pkgs.toot}/bin/toot post "hello world Jamy here"
-    echo "y" | ${pkgs.toot}/bin/toot timeline | grep -F -q "hello world Jamy here"
+
+    # Retrieving timeline with toot currently broken due to incompatible timestamp format
+    # cf. <https://akkoma.dev/AkkomaGang/akkoma/issues/637> and <https://github.com/ihabunek/toot/issues/399>
+    #echo "y" | ${pkgs.toot}/bin/toot timeline | grep -F -q "hello world Jamy here"
 
     # Test file upload
     echo "y" | ${pkgs.toot}/bin/toot upload <(dd if=/dev/zero bs=1024 count=1024 status=none) \
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 4b338dac69a7..e5affdab8890 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -109,6 +109,7 @@ in {
   allTerminfo = handleTest ./all-terminfo.nix {};
   alps = handleTest ./alps.nix {};
   amazon-init-shell = handleTest ./amazon-init-shell.nix {};
+  anbox = runTest ./anbox.nix;
   anuko-time-tracker = handleTest ./anuko-time-tracker.nix {};
   apcupsd = handleTest ./apcupsd.nix {};
   apfs = runTest ./apfs.nix;
@@ -210,6 +211,8 @@ in {
   custom-ca = handleTest ./custom-ca.nix {};
   croc = handleTest ./croc.nix {};
   darling = handleTest ./darling.nix {};
+  dae = handleTest ./dae.nix {};
+  dconf = handleTest ./dconf.nix {};
   deepin = handleTest ./deepin.nix {};
   deluge = handleTest ./deluge.nix {};
   dendrite = handleTest ./matrix/dendrite.nix {};
@@ -341,13 +344,16 @@ in {
   hbase2 = handleTest ./hbase.nix { package=pkgs.hbase2; };
   hbase_2_4 = handleTest ./hbase.nix { package=pkgs.hbase_2_4; };
   hbase3 = handleTest ./hbase.nix { package=pkgs.hbase3; };
+  hddfancontrol = handleTest ./hddfancontrol.nix {};
   hedgedoc = handleTest ./hedgedoc.nix {};
   herbstluftwm = handleTest ./herbstluftwm.nix {};
   homepage-dashboard = handleTest ./homepage-dashboard.nix {};
+  honk = runTest ./honk.nix;
   installed-tests = pkgs.recurseIntoAttrs (handleTest ./installed-tests {});
   invidious = handleTest ./invidious.nix {};
   oci-containers = handleTestOn ["aarch64-linux" "x86_64-linux"] ./oci-containers.nix {};
   odoo = handleTest ./odoo.nix {};
+  odoo15 = handleTest ./odoo.nix { package = pkgs.odoo15; };
   # 9pnet_virtio used to mount /nix partition doesn't support
   # hibernation. This test happens to work on x86_64-linux but
   # not on other platforms.
@@ -391,6 +397,7 @@ in {
   jibri = handleTest ./jibri.nix {};
   jirafeau = handleTest ./jirafeau.nix {};
   jitsi-meet = handleTest ./jitsi-meet.nix {};
+  jool = import ./jool.nix { inherit pkgs runTest; };
   k3s = handleTest ./k3s {};
   kafka = handleTest ./kafka.nix {};
   kanidm = handleTest ./kanidm.nix {};
@@ -432,7 +439,7 @@ in {
   lightdm = handleTest ./lightdm.nix {};
   lighttpd = handleTest ./lighttpd.nix {};
   limesurvey = handleTest ./limesurvey.nix {};
-  listmonk = handleTest ./listmonk.nix {};
+  listmonk = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./listmonk.nix {};
   litestream = handleTest ./litestream.nix {};
   lldap = handleTest ./lldap.nix {};
   locate = handleTest ./locate.nix {};
@@ -441,10 +448,8 @@ in {
   loki = handleTest ./loki.nix {};
   luks = handleTest ./luks.nix {};
   lvm2 = handleTest ./lvm2 {};
-  lxd = handleTest ./lxd.nix {};
-  lxd-nftables = handleTest ./lxd-nftables.nix {};
+  lxd = pkgs.recurseIntoAttrs (handleTest ./lxd { inherit handleTestOn; });
   lxd-image-server = handleTest ./lxd-image-server.nix {};
-  lxd-ui = handleTest ./lxd-ui.nix {};
   #logstash = handleTest ./logstash.nix {};
   lorri = handleTest ./lorri/default.nix {};
   maddy = discoverTests (import ./maddy { inherit handleTest; });
@@ -524,6 +529,7 @@ in {
   networking.scripted = handleTest ./networking.nix { networkd = false; };
   netbox = handleTest ./web-apps/netbox.nix { inherit (pkgs) netbox; };
   netbox_3_3 = handleTest ./web-apps/netbox.nix { netbox = pkgs.netbox_3_3; };
+  netbox-upgrade = handleTest ./web-apps/netbox-upgrade.nix {};
   # TODO: put in networking.nix after the test becomes more complete
   networkingProxy = handleTest ./networking-proxy.nix {};
   nextcloud = handleTest ./nextcloud {};
@@ -671,6 +677,7 @@ in {
   rabbitmq = handleTest ./rabbitmq.nix {};
   radarr = handleTest ./radarr.nix {};
   radicale = handleTest ./radicale.nix {};
+  ragnarwm = handleTest ./ragnarwm.nix {};
   rasdaemon = handleTest ./rasdaemon.nix {};
   readarr = handleTest ./readarr.nix {};
   redis = handleTest ./redis.nix {};
@@ -723,6 +730,7 @@ in {
   sslh = handleTest ./sslh.nix {};
   sssd = handleTestOn ["x86_64-linux"] ./sssd.nix {};
   sssd-ldap = handleTestOn ["x86_64-linux"] ./sssd-ldap.nix {};
+  stalwart-mail = handleTest ./stalwart-mail.nix {};
   stargazer = runTest ./web-servers/stargazer.nix;
   starship = handleTest ./starship.nix {};
   static-web-server = handleTest ./web-servers/static-web-server.nix {};
@@ -830,6 +838,7 @@ in {
   uptime-kuma = handleTest ./uptime-kuma.nix {};
   usbguard = handleTest ./usbguard.nix {};
   user-activation-scripts = handleTest ./user-activation-scripts.nix {};
+  user-expiry = runTest ./user-expiry.nix;
   user-home-mode = handleTest ./user-home-mode.nix {};
   uwsgi = handleTest ./uwsgi.nix {};
   v2ray = handleTest ./v2ray.nix {};
diff --git a/nixos/tests/anbox.nix b/nixos/tests/anbox.nix
new file mode 100644
index 000000000000..d78f63ec761f
--- /dev/null
+++ b/nixos/tests/anbox.nix
@@ -0,0 +1,40 @@
+{ lib, pkgs, ... }:
+
+{
+  name = "anbox";
+  meta.maintainers = with lib.maintainers; [ mvnetbiz ];
+
+  nodes.machine = { pkgs, config, ... }: {
+    imports = [
+      ./common/user-account.nix
+      ./common/x11.nix
+    ];
+
+    environment.systemPackages = with pkgs; [ android-tools ];
+
+    test-support.displayManager.auto.user = "alice";
+
+    virtualisation.anbox.enable = true;
+    boot.kernelPackages = pkgs.linuxPackages_5_15;
+
+    # The AArch64 anbox image will not start.
+    # Meanwhile the postmarketOS images work just fine.
+    virtualisation.anbox.image = pkgs.anbox.postmarketos-image;
+    virtualisation.memorySize = 2500;
+  };
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.users.users.alice;
+    bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${toString user.uid}/bus";
+  in ''
+    machine.wait_for_x()
+
+    machine.wait_until_succeeds(
+        "sudo -iu alice ${bus} anbox wait-ready"
+    )
+
+    machine.wait_until_succeeds("adb shell true")
+
+    print(machine.succeed("adb devices"))
+  '';
+}
diff --git a/nixos/tests/caddy.nix b/nixos/tests/caddy.nix
index 238091ec606f..5a0d3539394b 100644
--- a/nixos/tests/caddy.nix
+++ b/nixos/tests/caddy.nix
@@ -34,6 +34,20 @@ import ./make-test-python.nix ({ pkgs, ... }: {
           "http://localhost:8081" = { };
         };
       };
+      specialisation.rfc42.configuration = {
+        services.caddy.settings = {
+          apps.http.servers.default = {
+            listen = [ ":80" ];
+            routes = [{
+              handle = [{
+                body = "hello world";
+                handler = "static_response";
+                status_code = 200;
+              }];
+            }];
+          };
+        };
+      };
     };
   };
 
@@ -41,6 +55,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     let
       justReloadSystem = "${nodes.webserver.system.build.toplevel}/specialisation/config-reload";
       multipleConfigs = "${nodes.webserver.system.build.toplevel}/specialisation/multiple-configs";
+      rfc42Config = "${nodes.webserver.system.build.toplevel}/specialisation/rfc42";
     in
     ''
       url = "http://localhost/example.html"
@@ -62,5 +77,12 @@ import ./make-test-python.nix ({ pkgs, ... }: {
           )
           webserver.wait_for_open_port(8080)
           webserver.wait_for_open_port(8081)
+
+      with subtest("rfc42 settings config"):
+          webserver.succeed(
+              "${rfc42Config}/bin/switch-to-configuration test >&2"
+          )
+          webserver.wait_for_open_port(80)
+          webserver.succeed("curl http://localhost | grep hello")
     '';
 })
diff --git a/nixos/tests/common/lxd/config.yaml b/nixos/tests/common/lxd/config.yaml
deleted file mode 100644
index 3bb667ed43f7..000000000000
--- a/nixos/tests/common/lxd/config.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-storage_pools:
-  - name: default
-    driver: dir
-    config:
-      source: /var/lxd-pool
-
-networks:
-  - name: lxdbr0
-    type: bridge
-    config:
-      ipv4.address: auto
-      ipv6.address: none
-
-profiles:
-  - name: default
-    devices:
-      eth0:
-        name: eth0
-        network: lxdbr0
-        type: nic
-      root:
-        path: /
-        pool: default
-        type: disk
diff --git a/nixos/tests/custom-ca.nix b/nixos/tests/custom-ca.nix
index 25a7b6fdea46..0fcdf81022d7 100644
--- a/nixos/tests/custom-ca.nix
+++ b/nixos/tests/custom-ca.nix
@@ -131,8 +131,8 @@ let
         # chromium-based browsers refuse to run as root
         test-support.displayManager.auto.user = "alice";
 
-        # browsers may hang with the default memory
-        virtualisation.memorySize = 600;
+        # machine often runs out of memory with less
+        virtualisation.memorySize = 1024;
 
         environment.systemPackages = [ pkgs.xdotool pkgs.${browser} ];
       };
diff --git a/nixos/tests/dae.nix b/nixos/tests/dae.nix
new file mode 100644
index 000000000000..b8c8ebce7457
--- /dev/null
+++ b/nixos/tests/dae.nix
@@ -0,0 +1,29 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+
+  name = "dae";
+
+  meta = {
+    maintainers = with lib.maintainers; [ oluceps ];
+  };
+
+  nodes.machine = { pkgs, ... }: {
+    environment.systemPackages = [ pkgs.curl ];
+    services.nginx = {
+      enable = true;
+      statusPage = true;
+    };
+    services.dae = {
+      enable = true;
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("nginx.service")
+    machine.wait_for_unit("dae.service")
+
+    machine.wait_for_open_port(80)
+
+    machine.succeed("curl --fail --max-time 10 http://localhost")
+  '';
+
+})
diff --git a/nixos/tests/dconf.nix b/nixos/tests/dconf.nix
new file mode 100644
index 000000000000..86f703e3b98e
--- /dev/null
+++ b/nixos/tests/dconf.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix
+  ({ lib, ... }:
+  {
+    name = "dconf";
+
+    meta.maintainers = with lib.maintainers; [
+      linsui
+    ];
+
+    nodes.machine = { config, pkgs, lib, ... }: {
+      users.extraUsers.alice = { isNormalUser = true; };
+      programs.dconf = with lib.gvariant; {
+        enable = true;
+        profiles.user.databases = [
+          {
+            settings = {
+              "test/not/locked" = mkInt32 1;
+              "test/is/locked" = "locked";
+            };
+            locks = [
+              "/test/is/locked"
+            ];
+          }
+        ];
+      };
+    };
+
+    testScript = ''
+      machine.succeed("test $(dconf read -d /test/not/locked) == 1")
+      machine.succeed("test $(dconf read -d /test/is/locked) == \"'locked'\"")
+      machine.fail("sudo -u alice dbus-run-session -- dconf write /test/is/locked \"@s 'unlocked'\"")
+      machine.succeed("sudo -u alice dbus-run-session -- dconf write /test/not/locked \"@i 2\"")
+    '';
+  })
diff --git a/nixos/tests/dolibarr.nix b/nixos/tests/dolibarr.nix
index 2f012a0c67da..4fdee9e9698f 100644
--- a/nixos/tests/dolibarr.nix
+++ b/nixos/tests/dolibarr.nix
@@ -50,7 +50,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     # Now, we have installed the machine, let's verify we still have the right configuration.
     assert 'nixos' in machine.succeed("cat /var/lib/dolibarr/conf.php")
     # We do not want any redirect now as we have installed the machine.
-    machine.succeed('curl -f -X POST http://localhost')
+    machine.succeed('curl -f -X GET http://localhost')
     # Test authentication to the webservice.
     parser = TokenParser()
     parser.feed(machine.succeed('curl -f -X GET http://localhost/index.php?mainmenu=login&username=root'))
diff --git a/nixos/tests/hddfancontrol.nix b/nixos/tests/hddfancontrol.nix
new file mode 100644
index 000000000000..b5fa7ccb2c19
--- /dev/null
+++ b/nixos/tests/hddfancontrol.nix
@@ -0,0 +1,44 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "hddfancontrol";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ benley ];
+  };
+
+  nodes.machine = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    services.hddfancontrol.enable = true;
+    services.hddfancontrol.disks = ["/dev/vda"];
+    services.hddfancontrol.pwmPaths = ["/test/hwmon1/pwm1"];
+    services.hddfancontrol.extraArgs = ["--pwm-start-value=32"
+                                        "--pwm-stop-value=0"];
+
+    systemd.services.hddfancontrol_fixtures = {
+      description = "Install test fixtures for hddfancontrol";
+      serviceConfig = {
+        Type = "oneshot";
+      };
+      script = ''
+        mkdir -p /test/hwmon1
+        echo 255 > /test/hwmon1/pwm1
+        echo 2 > /test/hwmon1/pwm1_enable
+      '';
+      wantedBy = ["hddfancontrol.service"];
+      before = ["hddfancontrol.service"];
+    };
+
+    systemd.services.hddfancontrol.serviceConfig.ReadWritePaths = "/test";
+  };
+
+  # hddfancontrol.service will fail to start because qemu /dev/vda doesn't have
+  # any thermal interfaces, but it should ensure that fans appear to be running
+  # before it aborts.
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("journalctl -eu hddfancontrol.service|grep 'Setting fan speed'")
+    machine.shutdown()
+
+  '';
+
+})
diff --git a/nixos/tests/honk.nix b/nixos/tests/honk.nix
new file mode 100644
index 000000000000..71d86a592439
--- /dev/null
+++ b/nixos/tests/honk.nix
@@ -0,0 +1,32 @@
+{ lib, ... }:
+
+{
+  name = "honk-server";
+
+  nodes = {
+    machine = { pkgs, ... }: {
+      services.honk = {
+        enable = true;
+        host = "0.0.0.0";
+        port = 8080;
+        username = "username";
+        passwordFile = "${pkgs.writeText "honk-password" "secure"}";
+        servername = "servername";
+      };
+    };
+  };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("honk.service")
+    machine.wait_for_open_port(8080)
+
+    machine.stop_job("honk")
+    machine.wait_for_closed_port(8080)
+
+    machine.start_job("honk")
+    machine.wait_for_open_port(8080)
+  '';
+
+  meta.maintainers = [ lib.maintainers.drupol ];
+}
diff --git a/nixos/tests/influxdb2.nix b/nixos/tests/influxdb2.nix
index c9c54b788cc0..1631ac1d9408 100644
--- a/nixos/tests/influxdb2.nix
+++ b/nixos/tests/influxdb2.nix
@@ -6,6 +6,9 @@ import ./make-test-python.nix ({ pkgs, ...} : {
 
   nodes.machine = { lib, ... }: {
     environment.systemPackages = [ pkgs.influxdb2-cli ];
+    # Make sure that the service is restarted immediately if tokens need to be rewritten
+    # without relying on any Restart=on-failure behavior
+    systemd.services.influxdb2.serviceConfig.RestartSec = 6000;
     services.influxdb2.enable = true;
     services.influxdb2.provision = {
       enable = true;
@@ -15,22 +18,208 @@ import ./make-test-python.nix ({ pkgs, ...} : {
         passwordFile = pkgs.writeText "admin-pw" "ExAmPl3PA55W0rD";
         tokenFile = pkgs.writeText "admin-token" "verysecureadmintoken";
       };
+      organizations.someorg = {
+        buckets.somebucket = {};
+        auths.sometoken = {
+          description = "some auth token";
+          readBuckets = ["somebucket"];
+          writeBuckets = ["somebucket"];
+        };
+      };
+      users.someuser.passwordFile = pkgs.writeText "tmp-pw" "abcgoiuhaoga";
+    };
+
+    specialisation.withModifications.configuration = { ... }: {
+      services.influxdb2.provision = {
+        organizations.someorg.buckets.somebucket.present = false;
+        organizations.someorg.auths.sometoken.present = false;
+        users.someuser.present = false;
+
+        organizations.myorg = {
+          description = "Myorg description";
+          buckets.mybucket = {
+            description = "Mybucket description";
+          };
+          auths.mytoken = {
+            operator = true;
+            description = "operator token";
+            tokenFile = pkgs.writeText "tmp-tok" "someusertoken";
+          };
+        };
+        users.myuser.passwordFile = pkgs.writeText "tmp-pw" "abcgoiuhaoga";
+      };
+    };
+
+    specialisation.withParentDelete.configuration = { ... }: {
+      services.influxdb2.provision = {
+        organizations.someorg.present = false;
+        # Deleting the parent implies:
+        #organizations.someorg.buckets.somebucket.present = false;
+        #organizations.someorg.auths.sometoken.present = false;
+      };
+    };
+
+    specialisation.withNewTokens.configuration = { ... }: {
+      services.influxdb2.provision = {
+        organizations.default = {
+          auths.operator = {
+            operator = true;
+            description = "new optoken";
+            tokenFile = pkgs.writeText "tmp-tok" "newoptoken";
+          };
+          auths.allaccess = {
+            operator = true;
+            description = "new allaccess";
+            tokenFile = pkgs.writeText "tmp-tok" "newallaccess";
+          };
+          auths.specifics = {
+            description = "new specifics";
+            readPermissions = ["users" "tasks"];
+            writePermissions = ["tasks"];
+            tokenFile = pkgs.writeText "tmp-tok" "newspecificstoken";
+          };
+        };
+      };
     };
   };
 
   testScript = { nodes, ... }:
     let
+      specialisations = "${nodes.machine.system.build.toplevel}/specialisation";
       tokenArg = "--token verysecureadmintoken";
     in ''
+      def assert_contains(haystack, needle):
+          if needle not in haystack:
+              print("The haystack that will cause the following exception is:")
+              print("---")
+              print(haystack)
+              print("---")
+              raise Exception(f"Expected string '{needle}' was not found")
+
+      def assert_lacks(haystack, needle):
+          if needle in haystack:
+              print("The haystack that will cause the following exception is:")
+              print("---")
+              print(haystack, end="")
+              print("---")
+              raise Exception(f"Unexpected string '{needle}' was found")
+
       machine.wait_for_unit("influxdb2.service")
 
       machine.fail("curl --fail -X POST 'http://localhost:8086/api/v2/signin' -u admin:wrongpassword")
       machine.succeed("curl --fail -X POST 'http://localhost:8086/api/v2/signin' -u admin:ExAmPl3PA55W0rD")
 
       out = machine.succeed("influx org list ${tokenArg}")
-      assert "default" in out
+      assert_contains(out, "default")
+      assert_lacks(out, "myorg")
+      assert_contains(out, "someorg")
 
       out = machine.succeed("influx bucket list ${tokenArg} --org default")
-      assert "default" in out
+      assert_contains(out, "default")
+
+      machine.fail("influx bucket list ${tokenArg} --org myorg")
+
+      out = machine.succeed("influx bucket list ${tokenArg} --org someorg")
+      assert_contains(out, "somebucket")
+
+      out = machine.succeed("influx user list ${tokenArg}")
+      assert_contains(out, "admin")
+      assert_lacks(out, "myuser")
+      assert_contains(out, "someuser")
+
+      out = machine.succeed("influx auth list ${tokenArg}")
+      assert_lacks(out, "operator token")
+      assert_contains(out, "some auth token")
+
+      with subtest("withModifications"):
+        machine.succeed('${specialisations}/withModifications/bin/switch-to-configuration test')
+        machine.wait_for_unit("influxdb2.service")
+
+        out = machine.succeed("influx org list ${tokenArg}")
+        assert_contains(out, "default")
+        assert_contains(out, "myorg")
+        assert_contains(out, "someorg")
+
+        out = machine.succeed("influx bucket list ${tokenArg} --org myorg")
+        assert_contains(out, "mybucket")
+
+        out = machine.succeed("influx bucket list ${tokenArg} --org someorg")
+        assert_lacks(out, "somebucket")
+
+        out = machine.succeed("influx user list ${tokenArg}")
+        assert_contains(out, "admin")
+        assert_contains(out, "myuser")
+        assert_lacks(out, "someuser")
+
+        out = machine.succeed("influx auth list ${tokenArg}")
+        assert_contains(out, "operator token")
+        assert_lacks(out, "some auth token")
+
+        # Make sure the user token is also usable
+        machine.succeed("influx auth list --token someusertoken")
+
+      with subtest("keepsUnrelated"):
+        machine.succeed('${nodes.machine.system.build.toplevel}/bin/switch-to-configuration test')
+        machine.wait_for_unit("influxdb2.service")
+
+        out = machine.succeed("influx org list ${tokenArg}")
+        assert_contains(out, "default")
+        assert_contains(out, "myorg")
+        assert_contains(out, "someorg")
+
+        out = machine.succeed("influx bucket list ${tokenArg} --org default")
+        assert_contains(out, "default")
+
+        out = machine.succeed("influx bucket list ${tokenArg} --org myorg")
+        assert_contains(out, "mybucket")
+
+        out = machine.succeed("influx bucket list ${tokenArg} --org someorg")
+        assert_contains(out, "somebucket")
+
+        out = machine.succeed("influx user list ${tokenArg}")
+        assert_contains(out, "admin")
+        assert_contains(out, "myuser")
+        assert_contains(out, "someuser")
+
+        out = machine.succeed("influx auth list ${tokenArg}")
+        assert_contains(out, "operator token")
+        assert_contains(out, "some auth token")
+
+      with subtest("withParentDelete"):
+        machine.succeed('${specialisations}/withParentDelete/bin/switch-to-configuration test')
+        machine.wait_for_unit("influxdb2.service")
+
+        out = machine.succeed("influx org list ${tokenArg}")
+        assert_contains(out, "default")
+        assert_contains(out, "myorg")
+        assert_lacks(out, "someorg")
+
+        out = machine.succeed("influx bucket list ${tokenArg} --org default")
+        assert_contains(out, "default")
+
+        out = machine.succeed("influx bucket list ${tokenArg} --org myorg")
+        assert_contains(out, "mybucket")
+
+        machine.fail("influx bucket list ${tokenArg} --org someorg")
+
+        out = machine.succeed("influx user list ${tokenArg}")
+        assert_contains(out, "admin")
+        assert_contains(out, "myuser")
+        assert_contains(out, "someuser")
+
+        out = machine.succeed("influx auth list ${tokenArg}")
+        assert_contains(out, "operator token")
+        assert_lacks(out, "some auth token")
+
+      with subtest("withNewTokens"):
+        machine.succeed('${specialisations}/withNewTokens/bin/switch-to-configuration test')
+        machine.wait_for_unit("influxdb2.service")
+
+        out = machine.succeed("influx auth list ${tokenArg}")
+        assert_contains(out, "operator token")
+        assert_contains(out, "some auth token")
+        assert_contains(out, "new optoken")
+        assert_contains(out, "new allaccess")
+        assert_contains(out, "new specifics")
     '';
 })
diff --git a/nixos/tests/jool.nix b/nixos/tests/jool.nix
new file mode 100644
index 000000000000..93575f07b1c8
--- /dev/null
+++ b/nixos/tests/jool.nix
@@ -0,0 +1,220 @@
+{ pkgs, runTest }:
+
+let
+  inherit (pkgs) lib;
+
+  ipv6Only = {
+    networking.useDHCP = false;
+    networking.interfaces.eth1.ipv4.addresses = lib.mkVMOverride [ ];
+  };
+
+  ipv4Only = {
+    networking.useDHCP = false;
+    networking.interfaces.eth1.ipv6.addresses = lib.mkVMOverride [ ];
+  };
+
+  webserver = ip: msg: {
+    systemd.services.webserver = {
+      description = "Mock webserver";
+      wants = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      script = ''
+        while true; do
+        {
+          printf 'HTTP/1.0 200 OK\n'
+          printf 'Content-Length: ${toString (1 + builtins.stringLength msg)}\n'
+          printf '\n${msg}\n\n'
+        } | ${pkgs.libressl.nc}/bin/nc -${toString ip}nvl 80
+        done
+      '';
+    };
+    networking.firewall.allowedTCPPorts = [ 80 ];
+  };
+
+in
+
+{
+  siit = runTest {
+    # This test simulates the setup described in [1] with two IPv6 and
+    # IPv4-only devices on different subnets communicating through a border
+    # relay running Jool in SIIT mode.
+    # [1]: https://nicmx.github.io/Jool/en/run-vanilla.html
+    name = "jool-siit";
+    meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+    # Border relay
+    nodes.relay = {
+      virtualisation.vlans = [ 1 2 ];
+
+      # Enable packet routing
+      boot.kernel.sysctl = {
+        "net.ipv6.conf.all.forwarding" = 1;
+        "net.ipv4.conf.all.forwarding" = 1;
+      };
+
+      networking.useDHCP = false;
+      networking.interfaces = lib.mkVMOverride {
+        eth1.ipv6.addresses = [ { address = "fd::198.51.100.1"; prefixLength = 120; } ];
+        eth2.ipv4.addresses = [ { address = "192.0.2.1";  prefixLength = 24; } ];
+      };
+
+      networking.jool.enable = true;
+      networking.jool.siit.default.global.pool6 = "fd::/96";
+    };
+
+    # IPv6 only node
+    nodes.alice = {
+      imports = [ ipv6Only (webserver 6 "Hello, Bob!") ];
+
+      virtualisation.vlans = [ 1 ];
+      networking.interfaces.eth1.ipv6 = {
+        addresses = [ { address = "fd::198.51.100.8"; prefixLength = 120; } ];
+        routes    = [ { address = "fd::192.0.2.0"; prefixLength = 120;
+                        via = "fd::198.51.100.1"; } ];
+      };
+    };
+
+    # IPv4 only node
+    nodes.bob = {
+      imports = [ ipv4Only (webserver 4 "Hello, Alice!") ];
+
+      virtualisation.vlans = [ 2 ];
+      networking.interfaces.eth1.ipv4 = {
+        addresses = [ { address = "192.0.2.16"; prefixLength = 24; } ];
+        routes    = [ { address = "198.51.100.0"; prefixLength = 24;
+                        via = "192.0.2.1"; } ];
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      relay.wait_for_unit("jool-siit-default.service")
+      alice.wait_for_unit("network-addresses-eth1.service")
+      bob.wait_for_unit("network-addresses-eth1.service")
+
+      with subtest("Alice and Bob can't ping each other"):
+        relay.systemctl("stop jool-siit-default.service")
+        alice.fail("ping -c1 fd::192.0.2.16")
+        bob.fail("ping -c1 198.51.100.8")
+
+      with subtest("Alice and Bob can ping using the relay"):
+        relay.systemctl("start jool-siit-default.service")
+        alice.wait_until_succeeds("ping -c1 fd::192.0.2.16")
+        bob.wait_until_succeeds("ping -c1 198.51.100.8")
+
+      with subtest("Alice can connect to Bob's webserver"):
+        bob.wait_for_open_port(80)
+        alice.succeed("curl -vvv http://[fd::192.0.2.16] >&2")
+        alice.succeed("curl --fail -s http://[fd::192.0.2.16] | grep -q Alice")
+
+      with subtest("Bob can connect to Alices's webserver"):
+        alice.wait_for_open_port(80)
+        bob.succeed("curl --fail -s http://198.51.100.8 | grep -q Bob")
+    '';
+  };
+
+  nat64 = runTest {
+    # This test simulates the setup described in [1] with two IPv6-only nodes
+    # (a client and a homeserver) on the LAN subnet and an IPv4 node on the WAN.
+    # The router runs Jool in stateful NAT64 mode, masquarading the LAN and
+    # forwarding ports using static BIB entries.
+    # [1]: https://nicmx.github.io/Jool/en/run-nat64.html
+    name = "jool-nat64";
+    meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
+    # Router
+    nodes.router = {
+      virtualisation.vlans = [ 1 2 ];
+
+      # Enable packet routing
+      boot.kernel.sysctl = {
+        "net.ipv6.conf.all.forwarding" = 1;
+        "net.ipv4.conf.all.forwarding" = 1;
+      };
+
+      networking.useDHCP = false;
+      networking.interfaces = lib.mkVMOverride {
+        eth1.ipv6.addresses = [ { address = "2001:db8::1"; prefixLength = 96; } ];
+        eth2.ipv4.addresses = [ { address = "203.0.113.1"; prefixLength = 24; } ];
+      };
+
+      networking.jool.enable = true;
+      networking.jool.nat64.default = {
+        bib = [
+          { # forward HTTP 203.0.113.1 (router) → 2001:db8::9 (homeserver)
+            "protocol"     = "TCP";
+            "ipv4 address" = "203.0.113.1#80";
+            "ipv6 address" = "2001:db8::9#80";
+          }
+        ];
+        pool4 = [
+          # Ports for dynamic translation
+          { protocol =  "TCP";  prefix = "203.0.113.1/32"; "port range" = "40001-65535"; }
+          { protocol =  "UDP";  prefix = "203.0.113.1/32"; "port range" = "40001-65535"; }
+          { protocol = "ICMP";  prefix = "203.0.113.1/32"; "port range" = "40001-65535"; }
+          # Ports for static BIB entries
+          { protocol =  "TCP";  prefix = "203.0.113.1/32"; "port range" = "80"; }
+        ];
+      };
+    };
+
+    # LAN client (IPv6 only)
+    nodes.client = {
+      imports = [ ipv6Only ];
+      virtualisation.vlans = [ 1 ];
+
+      networking.interfaces.eth1.ipv6 = {
+        addresses = [ { address = "2001:db8::8"; prefixLength = 96; } ];
+        routes    = [ { address = "64:ff9b::";   prefixLength = 96;
+                        via = "2001:db8::1"; } ];
+      };
+    };
+
+    # LAN server (IPv6 only)
+    nodes.homeserver = {
+      imports = [ ipv6Only (webserver 6 "Hello from IPv6!") ];
+
+      virtualisation.vlans = [ 1 ];
+      networking.interfaces.eth1.ipv6 = {
+        addresses = [ { address = "2001:db8::9"; prefixLength = 96; } ];
+        routes    = [ { address = "64:ff9b::";   prefixLength = 96;
+                        via = "2001:db8::1"; } ];
+      };
+    };
+
+    # WAN server (IPv4 only)
+    nodes.server = {
+      imports = [ ipv4Only (webserver 4 "Hello from IPv4!") ];
+
+      virtualisation.vlans = [ 2 ];
+      networking.interfaces.eth1.ipv4.addresses =
+        [ { address = "203.0.113.16"; prefixLength = 24; } ];
+    };
+
+    testScript = ''
+      start_all()
+
+      for node in [client, homeserver, server]:
+        node.wait_for_unit("network-addresses-eth1.service")
+
+      with subtest("Client can ping the WAN server"):
+        router.wait_for_unit("jool-nat64-default.service")
+        client.succeed("ping -c1 64:ff9b::203.0.113.16")
+
+      with subtest("Client can connect to the WAN webserver"):
+        server.wait_for_open_port(80)
+        client.succeed("curl --fail -s http://[64:ff9b::203.0.113.16] | grep -q IPv4!")
+
+      with subtest("Router BIB entries are correctly populated"):
+        router.succeed("jool bib display | grep -q 'Dynamic TCP.*2001:db8::8'")
+        router.succeed("jool bib display | grep -q 'Static TCP.*2001:db8::9'")
+
+      with subtest("WAN server can reach the LAN server"):
+        homeserver.wait_for_open_port(80)
+        server.succeed("curl --fail -s http://203.0.113.1 | grep -q IPv6!")
+    '';
+
+  };
+
+}
diff --git a/nixos/tests/listmonk.nix b/nixos/tests/listmonk.nix
index 91003653c09e..938c36026a7f 100644
--- a/nixos/tests/listmonk.nix
+++ b/nixos/tests/listmonk.nix
@@ -42,20 +42,27 @@ import ./make-test-python.nix ({ lib, ... }: {
     machine.wait_for_open_port(9000)
     machine.succeed("[[ -f /var/lib/listmonk/.db_settings_initialized ]]")
 
+    assert json.loads(machine.succeed(generate_listmonk_request("GET", 'health')))['data'], 'Health endpoint returned unexpected value'
+
+    # A sample subscriber is guaranteed to exist at install-time
+    # A sample transactional template is guaranteed to exist at install-time
+    subscribers = json.loads(machine.succeed(generate_listmonk_request('GET', "subscribers")))['data']['results']
+    templates = json.loads(machine.succeed(generate_listmonk_request('GET', "templates")))['data']
+    tx_template = templates[2]
+
     # Test transactional endpoint
-    # subscriber_id=1 is guaranteed to exist at install-time
-    # template_id=2 is guaranteed to exist at install-time and is a transactional template (1 is a campaign template).
-    machine.succeed(
-      generate_listmonk_request('POST', 'tx', data={'subscriber_id': 1, 'template_id': 2})
-    )
-    assert 'Welcome John Doe' in machine.succeed(
+    print(machine.succeed(
+      generate_listmonk_request('POST', 'tx', data={'subscriber_id': subscribers[0]['id'], 'template_id': tx_template['id']})
+    ))
+
+    assert 'Welcome Anon Doe' in machine.succeed(
         "curl --fail http://localhost:8025/api/v2/messages"
-    )
+    ), "Failed to find Welcome John Doe inside the messages API endpoint"
 
     # Test campaign endpoint
     # Based on https://github.com/knadh/listmonk/blob/174a48f252a146d7e69dab42724e3329dbe25ebe/cmd/campaigns.go#L549 as docs do not exist.
     campaign_data = json.loads(machine.succeed(
-      generate_listmonk_request('POST', 'campaigns/1/test', data={'template_id': 1, 'subscribers': ['john@example.com'], 'name': 'Test', 'subject': 'NixOS is great', 'lists': [1], 'messenger': 'email'})
+      generate_listmonk_request('POST', 'campaigns/1/test', data={'template_id': templates[0]['id'], 'subscribers': ['john@example.com'], 'name': 'Test', 'subject': 'NixOS is great', 'lists': [1], 'messenger': 'email'})
     ))
 
     assert campaign_data['data']  # This is a boolean asserting if the test was successful or not: https://github.com/knadh/listmonk/blob/174a48f252a146d7e69dab42724e3329dbe25ebe/cmd/campaigns.go#L626
diff --git a/nixos/tests/lxd-image-server.nix b/nixos/tests/lxd-image-server.nix
index e5a292b61bd9..d0afa495a5b1 100644
--- a/nixos/tests/lxd-image-server.nix
+++ b/nixos/tests/lxd-image-server.nix
@@ -61,14 +61,14 @@ in {
     machine.wait_for_unit("lxd.service")
     machine.wait_for_file("/var/lib/lxd/unix.socket")
 
-    # It takes additional second for lxd to settle
-    machine.sleep(1)
+    # Wait for lxd to settle
+    machine.succeed("lxd waitready")
 
     # lxd expects the pool's directory to already exist
     machine.succeed("mkdir /var/lxd-pool")
 
     machine.succeed(
-        "cat ${./common/lxd/config.yaml} | lxd init --preseed"
+        "lxd init --minimal"
     )
 
     machine.succeed(
diff --git a/nixos/tests/lxd.nix b/nixos/tests/lxd/container.nix
index 2c2c19e0eecf..bdaaebfc0028 100644
--- a/nixos/tests/lxd.nix
+++ b/nixos/tests/lxd/container.nix
@@ -1,7 +1,7 @@
-import ./make-test-python.nix ({ pkgs, lib, ... } :
+import ../make-test-python.nix ({ pkgs, lib, ... } :
 
 let
-  lxd-image = import ../release.nix {
+  releases = import ../../release.nix {
     configuration = {
       # Building documentation makes the test unnecessarily take a longer time:
       documentation.enable = lib.mkForce false;
@@ -11,14 +11,14 @@ let
     };
   };
 
-  lxd-image-metadata = lxd-image.lxdMeta.${pkgs.stdenv.hostPlatform.system};
-  lxd-image-rootfs = lxd-image.lxdImage.${pkgs.stdenv.hostPlatform.system};
+  lxd-image-metadata = releases.lxdContainerMeta.${pkgs.stdenv.hostPlatform.system};
+  lxd-image-rootfs = releases.lxdContainerImage.${pkgs.stdenv.hostPlatform.system};
 
 in {
-  name = "lxd";
+  name = "lxd-container";
 
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ patryk27 ];
+    maintainers = [ patryk27 adamcstephens ];
   };
 
   nodes.machine = { lib, ... }: {
@@ -38,19 +38,21 @@ in {
   };
 
   testScript = ''
+    def instance_is_up(_) -> bool:
+      status, _ = machine.execute("lxc exec container --disable-stdin --force-interactive /run/current-system/sw/bin/true")
+      return status == 0
+
     machine.wait_for_unit("sockets.target")
     machine.wait_for_unit("lxd.service")
     machine.wait_for_file("/var/lib/lxd/unix.socket")
 
-    # It takes additional second for lxd to settle
-    machine.sleep(1)
+    # Wait for lxd to settle
+    machine.succeed("lxd waitready")
 
-    # lxd expects the pool's directory to already exist
-    machine.succeed("mkdir /var/lxd-pool")
+    # no preseed should mean no service
+    machine.fail("systemctl status lxd-preseed.service")
 
-    machine.succeed(
-        "cat ${./common/lxd/config.yaml} | lxd init --preseed"
-    )
+    machine.succeed("lxd init --minimal")
 
     machine.succeed(
         "lxc image import ${lxd-image-metadata}/*/*.tar.xz ${lxd-image-rootfs}/*/*.tar.xz --alias nixos"
@@ -58,21 +60,23 @@ in {
 
     with subtest("Container can be managed"):
         machine.succeed("lxc launch nixos container")
-        machine.sleep(5)
+        with machine.nested("Waiting for instance to start and be usable"):
+          retry(instance_is_up)
         machine.succeed("echo true | lxc exec container /run/current-system/sw/bin/bash -")
-        machine.succeed("lxc exec container true")
         machine.succeed("lxc delete -f container")
 
     with subtest("Container is mounted with lxcfs inside"):
         machine.succeed("lxc launch nixos container")
-        machine.sleep(5)
+        with machine.nested("Waiting for instance to start and be usable"):
+            retry(instance_is_up)
 
         ## ---------- ##
         ## limits.cpu ##
 
         machine.succeed("lxc config set container limits.cpu 1")
         machine.succeed("lxc restart container")
-        machine.sleep(5)
+        with machine.nested("Waiting for instance to start and be usable"):
+            retry(instance_is_up)
 
         assert (
             "1"
@@ -81,7 +85,8 @@ in {
 
         machine.succeed("lxc config set container limits.cpu 2")
         machine.succeed("lxc restart container")
-        machine.sleep(5)
+        with machine.nested("Waiting for instance to start and be usable"):
+            retry(instance_is_up)
 
         assert (
             "2"
@@ -93,7 +98,8 @@ in {
 
         machine.succeed("lxc config set container limits.memory 64MB")
         machine.succeed("lxc restart container")
-        machine.sleep(5)
+        with machine.nested("Waiting for instance to start and be usable"):
+            retry(instance_is_up)
 
         assert (
             "MemTotal:          62500 kB"
@@ -102,7 +108,8 @@ in {
 
         machine.succeed("lxc config set container limits.memory 128MB")
         machine.succeed("lxc restart container")
-        machine.sleep(5)
+        with machine.nested("Waiting for instance to start and be usable"):
+            retry(instance_is_up)
 
         assert (
             "MemTotal:         125000 kB"
diff --git a/nixos/tests/lxd/default.nix b/nixos/tests/lxd/default.nix
new file mode 100644
index 000000000000..20afdd5e48bb
--- /dev/null
+++ b/nixos/tests/lxd/default.nix
@@ -0,0 +1,12 @@
+{
+  system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../../.. {inherit system config;},
+  handleTestOn,
+}: {
+  container = import ./container.nix {inherit system pkgs;};
+  nftables = import ./nftables.nix {inherit system pkgs;};
+  preseed = import ./preseed.nix {inherit system pkgs;};
+  ui = import ./ui.nix {inherit system pkgs;};
+  virtual-machine = handleTestOn ["x86_64-linux"] ./virtual-machine.nix { inherit system pkgs; };
+}
diff --git a/nixos/tests/lxd-nftables.nix b/nixos/tests/lxd/nftables.nix
index 293065001567..d98bd4952906 100644
--- a/nixos/tests/lxd-nftables.nix
+++ b/nixos/tests/lxd/nftables.nix
@@ -5,7 +5,7 @@
 # iptables to nftables requires a full reboot, which is a bit hard inside NixOS
 # tests.
 
-import ./make-test-python.nix ({ pkgs, ...} : {
+import ../make-test-python.nix ({ pkgs, ...} : {
   name = "lxd-nftables";
 
   meta = with pkgs.lib.maintainers; {
@@ -20,8 +20,8 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     networking = {
       firewall.enable = false;
       nftables.enable = true;
-      nftables.ruleset = ''
-        table inet filter {
+      nftables.tables."filter".family = "inet";
+      nftables.tables."filter".content = ''
           chain incoming {
             type filter hook input priority 0;
             policy accept;
@@ -36,7 +36,6 @@ import ./make-test-python.nix ({ pkgs, ...} : {
             type filter hook output priority 0;
             policy accept;
           }
-        }
       '';
     };
   };
diff --git a/nixos/tests/lxd/preseed.nix b/nixos/tests/lxd/preseed.nix
new file mode 100644
index 000000000000..7d89b9f56daa
--- /dev/null
+++ b/nixos/tests/lxd/preseed.nix
@@ -0,0 +1,71 @@
+import ../make-test-python.nix ({ pkgs, lib, ... } :
+
+{
+  name = "lxd-preseed";
+
+  meta = {
+    maintainers = with lib.maintainers; [ adamcstephens ];
+  };
+
+  nodes.machine = { lib, ... }: {
+    virtualisation = {
+      diskSize = 4096;
+
+      lxc.lxcfs.enable = true;
+      lxd.enable = true;
+
+      lxd.preseed = {
+        networks = [
+          {
+            name = "nixostestbr0";
+            type = "bridge";
+            config = {
+              "ipv4.address" = "10.0.100.1/24";
+              "ipv4.nat" = "true";
+            };
+          }
+        ];
+        profiles = [
+          {
+            name = "nixostest_default";
+            devices = {
+              eth0 = {
+                name = "eth0";
+                network = "nixostestbr0";
+                type = "nic";
+              };
+              root = {
+                path = "/";
+                pool = "default";
+                size = "35GiB";
+                type = "disk";
+              };
+            };
+          }
+        ];
+        storage_pools = [
+          {
+            name = "nixostest_pool";
+            driver = "dir";
+          }
+        ];
+      };
+    };
+  };
+
+  testScript = ''
+    def wait_for_preseed(_) -> bool:
+      _, output = machine.systemctl("is-active lxd-preseed.service")
+      return ("inactive" in output)
+
+    machine.wait_for_unit("sockets.target")
+    machine.wait_for_unit("lxd.service")
+    with machine.nested("Waiting for preseed to complete"):
+      retry(wait_for_preseed)
+
+    with subtest("Verify preseed resources created"):
+      machine.succeed("lxc profile show nixostest_default")
+      machine.succeed("lxc network info nixostestbr0")
+      machine.succeed("lxc storage show nixostest_pool")
+  '';
+})
diff --git a/nixos/tests/lxd-ui.nix b/nixos/tests/lxd/ui.nix
index 19eaa226c0bf..86cb30d8c2b6 100644
--- a/nixos/tests/lxd-ui.nix
+++ b/nixos/tests/lxd/ui.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ pkgs, lib, ... }: {
+import ../make-test-python.nix ({ pkgs, lib, ... }: {
   name = "lxd-ui";
 
   meta = with pkgs.lib.maintainers; {
diff --git a/nixos/tests/lxd/virtual-machine.nix b/nixos/tests/lxd/virtual-machine.nix
new file mode 100644
index 000000000000..93705e9350c5
--- /dev/null
+++ b/nixos/tests/lxd/virtual-machine.nix
@@ -0,0 +1,64 @@
+import ../make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+  releases = import ../../release.nix {
+    configuration = {
+      # Building documentation makes the test unnecessarily take a longer time:
+      documentation.enable = lib.mkForce false;
+
+      # Our tests require `grep` & friends:
+      environment.systemPackages = with pkgs; [busybox];
+    };
+  };
+
+  lxd-image-metadata = releases.lxdVirtualMachineImageMeta.${pkgs.stdenv.hostPlatform.system};
+  lxd-image-disk = releases.lxdVirtualMachineImage.${pkgs.stdenv.hostPlatform.system};
+
+  instance-name = "instance1";
+in {
+  name = "lxd-virtual-machine";
+
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [adamcstephens];
+  };
+
+  nodes.machine = {lib, ...}: {
+    virtualisation = {
+      diskSize = 4096;
+
+      cores = 2;
+
+      # Ensure we have enough memory for the nested virtual machine
+      memorySize = 1024;
+
+      lxc.lxcfs.enable = true;
+      lxd.enable = true;
+    };
+  };
+
+  testScript = ''
+    def instance_is_up(_) -> bool:
+      status, _ = machine.execute("lxc exec ${instance-name} --disable-stdin --force-interactive /run/current-system/sw/bin/true")
+      return status == 0
+
+    machine.wait_for_unit("sockets.target")
+    machine.wait_for_unit("lxd.service")
+    machine.wait_for_file("/var/lib/lxd/unix.socket")
+
+    # Wait for lxd to settle
+    machine.succeed("lxd waitready")
+
+    machine.succeed("lxd init --minimal")
+
+    with subtest("virtual-machine image can be imported"):
+        machine.succeed("lxc image import ${lxd-image-metadata}/*/*.tar.xz ${lxd-image-disk}/nixos.qcow2 --alias nixos")
+
+    with subtest("virtual-machine can be launched and become available"):
+        machine.succeed("lxc launch nixos ${instance-name} --vm --config limits.memory=512MB --config security.secureboot=false")
+        with machine.nested("Waiting for instance to start and be usable"):
+          retry(instance_is_up)
+
+    with subtest("lxd-agent is started"):
+        machine.succeed("lxc exec ${instance-name} systemctl is-active lxd-agent")
+  '';
+})
diff --git a/nixos/tests/odoo.nix b/nixos/tests/odoo.nix
index 7c2cf31370f9..00ae4a2137d1 100644
--- a/nixos/tests/odoo.nix
+++ b/nixos/tests/odoo.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ pkgs, lib, ...} : {
+import ./make-test-python.nix ({ pkgs, lib, package ? pkgs.odoo, ...} : {
   name = "odoo";
   meta.maintainers = with lib.maintainers; [ mkg20001 ];
 
@@ -11,6 +11,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
 
       services.odoo = {
         enable = true;
+        package = package;
         domain = "localhost";
       };
     };
diff --git a/nixos/tests/os-prober.nix b/nixos/tests/os-prober.nix
index 22e720824c80..dae1306bd69d 100644
--- a/nixos/tests/os-prober.nix
+++ b/nixos/tests/os-prober.nix
@@ -76,6 +76,7 @@ in {
       # nixos-rebuild needs must be included in the VM.
       system.extraDependencies = with pkgs;
         [
+          bintools
           brotli
           brotli.dev
           brotli.lib
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
index d86f8ac634e8..306c5e071e75 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -716,6 +716,41 @@ let
       '';
     };
 
+    mysqld = {
+      exporterConfig = {
+        enable = true;
+        runAsLocalSuperUser = true;
+        configFile = pkgs.writeText "test-prometheus-exporter-mysqld-config.my-cnf" ''
+          [client]
+          user = exporter
+          password = snakeoilpassword
+        '';
+      };
+      metricProvider = {
+        services.mysql = {
+          enable = true;
+          package = pkgs.mariadb;
+          initialScript = pkgs.writeText "mysql-init-script.sql" ''
+            CREATE USER 'exporter'@'localhost'
+            IDENTIFIED BY 'snakeoilpassword'
+            WITH MAX_USER_CONNECTIONS 3;
+            GRANT PROCESS, REPLICATION CLIENT, SLAVE MONITOR, SELECT ON *.* TO 'exporter'@'localhost';
+          '';
+        };
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-mysqld-exporter.service")
+        wait_for_open_port(9104)
+        wait_for_unit("mysql.service")
+        succeed("curl -sSf http://localhost:9104/metrics | grep 'mysql_up 1'")
+        systemctl("stop mysql.service")
+        succeed("curl -sSf http://localhost:9104/metrics | grep 'mysql_up 0'")
+        systemctl("start mysql.service")
+        wait_for_unit("mysql.service")
+        succeed("curl -sSf http://localhost:9104/metrics | grep 'mysql_up 1'")
+      '';
+    };
+
     nextcloud = {
       exporterConfig = {
         enable = true;
@@ -1387,8 +1422,7 @@ let
     unbound = {
       exporterConfig = {
         enable = true;
-        fetchType = "uds";
-        controlInterface = "/run/unbound/unbound.ctl";
+        unbound.host = "unix:///run/unbound/unbound.ctl";
       };
       metricProvider = {
         services.unbound = {
@@ -1403,7 +1437,7 @@ let
         wait_for_unit("unbound.service")
         wait_for_unit("prometheus-unbound-exporter.service")
         wait_for_open_port(9167)
-        succeed("curl -sSf localhost:9167/metrics | grep 'unbound_up 1'")
+        wait_until_succeeds("curl -sSf localhost:9167/metrics | grep 'unbound_up 1'")
       '';
     };
 
diff --git a/nixos/tests/ragnarwm.nix b/nixos/tests/ragnarwm.nix
new file mode 100644
index 000000000000..f7c588b92008
--- /dev/null
+++ b/nixos/tests/ragnarwm.nix
@@ -0,0 +1,32 @@
+import ./make-test-python.nix ({ lib, ...} : {
+  name = "ragnarwm";
+
+  meta = {
+    maintainers = with lib.maintainers; [ sigmanificient ];
+  };
+
+  nodes.machine = { pkgs, lib, ... }: {
+    imports = [ ./common/x11.nix ./common/user-account.nix ];
+    test-support.displayManager.auto.user = "alice";
+    services.xserver.displayManager.defaultSession = lib.mkForce "ragnar";
+    services.xserver.windowManager.ragnarwm.enable = true;
+
+    # Setup the default terminal of Ragnar
+    environment.systemPackages = [ pkgs.alacritty ];
+  };
+
+  testScript = ''
+    with subtest("ensure x starts"):
+        machine.wait_for_x()
+        machine.wait_for_file("/home/alice/.Xauthority")
+        machine.succeed("xauth merge ~alice/.Xauthority")
+
+    with subtest("ensure we can open a new terminal"):
+        # Sleeping a bit before the test, as it may help for sending keys
+        machine.sleep(2)
+        machine.send_key("meta_l-ret")
+        machine.wait_for_window(r"alice.*?machine")
+        machine.sleep(2)
+        machine.screenshot("terminal")
+  '';
+})
diff --git a/nixos/tests/stalwart-mail.nix b/nixos/tests/stalwart-mail.nix
new file mode 100644
index 000000000000..b5589966a160
--- /dev/null
+++ b/nixos/tests/stalwart-mail.nix
@@ -0,0 +1,117 @@
+# Rudimentary test checking that the Stalwart email server can:
+# - receive some message through SMTP submission, then
+# - serve this message through IMAP.
+
+let
+  certs = import ./common/acme/server/snakeoil-certs.nix;
+  domain = certs.domain;
+
+in import ./make-test-python.nix ({ lib, ... }: {
+  name = "stalwart-mail";
+
+  nodes.main = { pkgs, ... }: {
+    security.pki.certificateFiles = [ certs.ca.cert ];
+
+    services.stalwart-mail = {
+      enable = true;
+      settings = {
+        server.hostname = domain;
+
+        certificate."snakeoil" = {
+          cert = "file://${certs.${domain}.cert}";
+          private-key = "file://${certs.${domain}.key}";
+        };
+
+        server.tls = {
+          certificate = "snakeoil";
+          enable = true;
+          implicit = false;
+        };
+
+        server.listener = {
+          "smtp-submission" = {
+            bind = [ "[::]:587" ];
+            protocol = "smtp";
+          };
+
+          "imap" = {
+            bind = [ "[::]:143" ];
+            protocol = "imap";
+          };
+        };
+
+        session.auth.mechanisms = [ "PLAIN" ];
+        session.auth.directory = "in-memory";
+        jmap.directory = "in-memory";  # shared with imap
+
+        session.rcpt.directory = "in-memory";
+        queue.outbound.next-hop = [ "local" ];
+
+        directory."in-memory" = {
+          type = "memory";
+          users = [
+            {
+              name = "alice";
+              secret = "foobar";
+              email = [ "alice@${domain}" ];
+            }
+            {
+              name = "bob";
+              secret = "foobar";
+              email = [ "bob@${domain}" ];
+            }
+          ];
+        };
+      };
+    };
+
+    environment.systemPackages = [
+      (pkgs.writers.writePython3Bin "test-smtp-submission" { } ''
+        from smtplib import SMTP
+
+        with SMTP('localhost', 587) as smtp:
+            smtp.starttls()
+            smtp.login('alice', 'foobar')
+            smtp.sendmail(
+                'alice@${domain}',
+                'bob@${domain}',
+                """
+                    From: alice@${domain}
+                    To: bob@${domain}
+                    Subject: Some test message
+
+                    This is a test message.
+                """.strip()
+            )
+      '')
+
+      (pkgs.writers.writePython3Bin "test-imap-read" { } ''
+        from imaplib import IMAP4
+
+        with IMAP4('localhost') as imap:
+            imap.starttls()
+            imap.login('bob', 'foobar')
+            imap.select('"All Mail"')
+            status, [ref] = imap.search(None, 'ALL')
+            assert status == 'OK'
+            [msgId] = ref.split()
+            status, msg = imap.fetch(msgId, 'BODY[TEXT]')
+            assert status == 'OK'
+            assert msg[0][1].strip() == b'This is a test message.'
+      '')
+    ];
+  };
+
+  testScript = /* python */ ''
+    main.wait_for_unit("stalwart-mail.service")
+    main.wait_for_open_port(587)
+    main.wait_for_open_port(143)
+
+    main.succeed("test-smtp-submission")
+    main.succeed("test-imap-read")
+  '';
+
+  meta = {
+    maintainers = with lib.maintainers; [ happysalada pacien ];
+  };
+})
diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix
index 53595ae7d3e2..529a20864206 100644
--- a/nixos/tests/switch-test.nix
+++ b/nixos/tests/switch-test.nix
@@ -450,7 +450,7 @@ in {
           ];
         };
 
-        mountModified.configuration = {
+        mountOptionsModified.configuration = {
           systemd.mounts = [
             {
               description = "Testmount";
@@ -463,6 +463,19 @@ in {
           ];
         };
 
+        mountModified.configuration = {
+          systemd.mounts = [
+            {
+              description = "Testmount";
+              what = "ramfs";
+              type = "ramfs";
+              where = "/testmount";
+              options = "size=10M";
+              wantedBy = [ "local-fs.target" ];
+            }
+          ];
+        };
+
         timer.configuration = {
           systemd.timers.test-timer = {
             wantedBy = [ "timers.target" ];
@@ -1137,7 +1150,8 @@ in {
         switch_to_specialisation("${machine}", "mount")
         out = machine.succeed("mount | grep 'on /testmount'")
         assert_contains(out, "size=1024k")
-        out = switch_to_specialisation("${machine}", "mountModified")
+        # Changing options reloads the unit
+        out = switch_to_specialisation("${machine}", "mountOptionsModified")
         assert_lacks(out, "stopping the following units:")
         assert_lacks(out, "NOT restarting the following changed units:")
         assert_contains(out, "reloading the following units: testmount.mount\n")
@@ -1147,6 +1161,17 @@ in {
         # It changed
         out = machine.succeed("mount | grep 'on /testmount'")
         assert_contains(out, "size=10240k")
+        # Changing anything but `Options=` restarts the unit
+        out = switch_to_specialisation("${machine}", "mountModified")
+        assert_lacks(out, "stopping the following units:")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_contains(out, "\nrestarting the following units: testmount.mount\n")
+        assert_lacks(out, "\nstarting the following units:")
+        assert_lacks(out, "the following new units were started:")
+        # It changed
+        out = machine.succeed("mount | grep 'on /testmount'")
+        assert_contains(out, "ramfs")
 
     with subtest("timers"):
         switch_to_specialisation("${machine}", "timer")
diff --git a/nixos/tests/user-activation-scripts.nix b/nixos/tests/user-activation-scripts.nix
index 5df072ce0508..ebd96b019e92 100644
--- a/nixos/tests/user-activation-scripts.nix
+++ b/nixos/tests/user-activation-scripts.nix
@@ -8,6 +8,7 @@ import ./make-test-python.nix ({ lib, ... }: {
       initialPassword = "pass1";
       isNormalUser = true;
     };
+    systemd.user.tmpfiles.users.alice.rules = [ "r %h/file-to-remove" ];
   };
 
   testScript = ''
@@ -27,7 +28,9 @@ import ./make-test-python.nix ({ lib, ... }: {
     machine.wait_for_file("/home/alice/login-ok")
     verify_user_activation_run_count(1)
 
+    machine.succeed("touch /home/alice/file-to-remove")
     machine.succeed("/run/current-system/bin/switch-to-configuration test")
     verify_user_activation_run_count(2)
+    machine.succeed("[[ ! -f /home/alice/file-to-remove ]] || false")
   '';
 })
diff --git a/nixos/tests/user-expiry.nix b/nixos/tests/user-expiry.nix
new file mode 100644
index 000000000000..bcaed7a0ccb0
--- /dev/null
+++ b/nixos/tests/user-expiry.nix
@@ -0,0 +1,70 @@
+let
+  alice = "alice";
+  bob = "bob";
+  eve = "eve";
+  passwd = "pass1";
+in
+{
+  name = "user-expiry";
+
+  nodes = {
+    machine = {
+      users.users = {
+        ${alice} = {
+          initialPassword = passwd;
+          isNormalUser = true;
+          expires = "1990-01-01";
+        };
+        ${bob} = {
+          initialPassword = passwd;
+          isNormalUser = true;
+          expires = "2990-01-01";
+        };
+        ${eve} = {
+          initialPassword = passwd;
+          isNormalUser = true;
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    def switch_to_tty(tty_number):
+      machine.fail(f"pgrep -f 'agetty.*tty{tty_number}'")
+      machine.send_key(f"alt-f{tty_number}")
+      machine.wait_until_succeeds(f"[ $(fgconsole) = {tty_number} ]")
+      machine.wait_for_unit(f"getty@tty{tty_number}.service")
+      machine.wait_until_succeeds(f"pgrep -f 'agetty.*tty{tty_number}'")
+
+
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_for_unit("getty@tty1.service")
+
+    with subtest("${alice} cannot login"):
+      machine.wait_until_tty_matches("1", "login: ")
+      machine.send_chars("${alice}\n")
+      machine.wait_until_tty_matches("1", "Password: ")
+      machine.send_chars("${passwd}\n")
+
+      machine.wait_until_succeeds("journalctl --grep='account ${alice} has expired \\(account expired\\)'")
+      machine.wait_until_tty_matches("1", "login: ")
+
+    with subtest("${bob} can login"):
+      switch_to_tty(2)
+      machine.wait_until_tty_matches("2", "login: ")
+      machine.send_chars("${bob}\n")
+      machine.wait_until_tty_matches("2", "Password: ")
+      machine.send_chars("${passwd}\n")
+
+      machine.wait_until_succeeds("pgrep -u ${bob} bash")
+
+    with subtest("${eve} can login"):
+      switch_to_tty(3)
+      machine.wait_until_tty_matches("3", "login: ")
+      machine.send_chars("${eve}\n")
+      machine.wait_until_tty_matches("3", "Password: ")
+      machine.send_chars("${passwd}\n")
+
+      machine.wait_until_succeeds("pgrep -u ${eve} bash")
+  '';
+}
diff --git a/nixos/tests/virtualbox.nix b/nixos/tests/virtualbox.nix
index 062b125eb611..e522d0679e15 100644
--- a/nixos/tests/virtualbox.nix
+++ b/nixos/tests/virtualbox.nix
@@ -519,4 +519,4 @@ in mapAttrs (mkVBoxTest false vboxVMs) {
     destroy_vm_test1()
     destroy_vm_test2()
   '';
-} // (lib.optionalAttrs enableUnfree unfreeTests)
+} // (optionalAttrs enableUnfree unfreeTests)
diff --git a/nixos/tests/web-apps/netbox-upgrade.nix b/nixos/tests/web-apps/netbox-upgrade.nix
new file mode 100644
index 000000000000..602cf8d889d4
--- /dev/null
+++ b/nixos/tests/web-apps/netbox-upgrade.nix
@@ -0,0 +1,85 @@
+import ../make-test-python.nix ({ lib, pkgs, ... }: let
+  oldNetbox = pkgs.netbox_3_3;
+in {
+  name = "netbox-upgrade";
+
+  meta = with lib.maintainers; {
+    maintainers = [ minijackson ];
+  };
+
+  nodes.machine = { config, ... }: {
+    services.netbox = {
+      enable = true;
+      package = oldNetbox;
+      secretKeyFile = pkgs.writeText "secret" ''
+        abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
+      '';
+    };
+
+    services.nginx = {
+      enable = true;
+
+      recommendedProxySettings = true;
+
+      virtualHosts.netbox = {
+        default = true;
+        locations."/".proxyPass = "http://localhost:${toString config.services.netbox.port}";
+        locations."/static/".alias = "/var/lib/netbox/static/";
+      };
+    };
+
+    users.users.nginx.extraGroups = [ "netbox" ];
+
+    networking.firewall.allowedTCPPorts = [ 80 ];
+
+    specialisation.upgrade.configuration.services.netbox.package = lib.mkForce pkgs.netbox;
+  };
+
+  testScript = { nodes, ... }:
+    let
+      apiVersion = version: lib.pipe version [
+        (lib.splitString ".")
+        (lib.take 2)
+        (lib.concatStringsSep ".")
+      ];
+      oldApiVersion = apiVersion oldNetbox.version;
+      newApiVersion = apiVersion pkgs.netbox.version;
+    in
+    ''
+      start_all()
+      machine.wait_for_unit("netbox.target")
+      machine.wait_for_unit("nginx.service")
+      machine.wait_until_succeeds("journalctl --since -1m --unit netbox --grep Listening")
+
+      def api_version(headers):
+          header = [header for header in headers.splitlines() if header.startswith("API-Version:")][0]
+          return header.split()[1]
+
+      def check_api_version(version):
+          headers = machine.succeed(
+            "curl -sSfL http://localhost/api/ --head -H 'Content-Type: application/json'"
+          )
+          assert api_version(headers) == version
+
+      with subtest("NetBox version is the old one"):
+          check_api_version("${oldApiVersion}")
+
+      # Somehow, even though netbox-housekeeping.service has After=netbox.service,
+      # netbox-housekeeping.service and netbox.service still get started at the
+      # same time, making netbox-housekeeping fail (can't really do some house
+      # keeping job if the database is not correctly formed).
+      #
+      # So we don't check that the upgrade went well, we just check that
+      # netbox.service is active, and that netbox-housekeeping can be run
+      # successfully afterwards.
+      #
+      # This is not good UX, but the system should be working nonetheless.
+      machine.execute("${nodes.machine.system.build.toplevel}/specialisation/upgrade/bin/switch-to-configuration test >&2")
+
+      machine.wait_for_unit("netbox.service")
+      machine.succeed("systemctl start netbox-housekeeping.service")
+
+      with subtest("NetBox version is the new one"):
+          check_api_version("${newApiVersion}")
+    '';
+})