about summary refs log tree commit diff
path: root/nixpkgs/nixos
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2023-11-05 16:15:01 +0100
committerAlyssa Ross <hi@alyssa.is>2023-11-05 16:15:01 +0100
commita2c1eff83c3118a9aee8076c7f84f58137416b6e (patch)
tree4668c9ad2bba229a0eb3ccc8adbe78402e04ab07 /nixpkgs/nixos
parentfa7e5142244bb8fd1c51b66df6e623a7f41cc0d3 (diff)
parent85f1ba3e51676fa8cc604a3d863d729026a6b8eb (diff)
downloadnixlib-a2c1eff83c3118a9aee8076c7f84f58137416b6e.tar
nixlib-a2c1eff83c3118a9aee8076c7f84f58137416b6e.tar.gz
nixlib-a2c1eff83c3118a9aee8076c7f84f58137416b6e.tar.bz2
nixlib-a2c1eff83c3118a9aee8076c7f84f58137416b6e.tar.lz
nixlib-a2c1eff83c3118a9aee8076c7f84f58137416b6e.tar.xz
nixlib-a2c1eff83c3118a9aee8076c7f84f58137416b6e.tar.zst
nixlib-a2c1eff83c3118a9aee8076c7f84f58137416b6e.zip
Merge branch 'nixos-unstable' of https://github.com/NixOS/nixpkgs into HEAD
Diffstat (limited to 'nixpkgs/nixos')
-rw-r--r--nixpkgs/nixos/doc/manual/configuration/x-windows.chapter.md6
-rw-r--r--nixpkgs/nixos/doc/manual/release-notes/rl-2311.section.md9
-rw-r--r--nixpkgs/nixos/modules/installer/cd-dvd/channel.nix54
-rw-r--r--nixpkgs/nixos/modules/module-list.nix2
-rw-r--r--nixpkgs/nixos/modules/programs/firefox.nix2
-rw-r--r--nixpkgs/nixos/modules/security/sudo.nix4
-rw-r--r--nixpkgs/nixos/modules/services/continuous-integration/woodpecker/server.nix8
-rw-r--r--nixpkgs/nixos/modules/services/hardware/fwupd.nix13
-rw-r--r--nixpkgs/nixos/modules/services/hardware/throttled.nix1
-rw-r--r--nixpkgs/nixos/modules/services/monitoring/grafana-image-renderer.nix2
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/akkoma.nix6
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/freshrss.nix6
-rw-r--r--nixpkgs/nixos/modules/services/web-apps/mobilizon.nix4
-rw-r--r--nixpkgs/nixos/modules/services/x11/extra-layouts.nix42
-rw-r--r--nixpkgs/nixos/modules/virtualisation/incus.nix236
-rw-r--r--nixpkgs/nixos/tests/all-tests.nix2
-rw-r--r--nixpkgs/nixos/tests/incus/container.nix77
-rw-r--r--nixpkgs/nixos/tests/incus/default.nix14
-rw-r--r--nixpkgs/nixos/tests/incus/preseed.nix60
-rw-r--r--nixpkgs/nixos/tests/incus/socket-activated.nix26
-rw-r--r--nixpkgs/nixos/tests/incus/virtual-machine.nix55
-rw-r--r--nixpkgs/nixos/tests/keymap.nix2
-rw-r--r--nixpkgs/nixos/tests/ssh-audit.nix103
23 files changed, 667 insertions, 67 deletions
diff --git a/nixpkgs/nixos/doc/manual/configuration/x-windows.chapter.md b/nixpkgs/nixos/doc/manual/configuration/x-windows.chapter.md
index 5a870a46cbb8..0451e4d25265 100644
--- a/nixpkgs/nixos/doc/manual/configuration/x-windows.chapter.md
+++ b/nixpkgs/nixos/doc/manual/configuration/x-windows.chapter.md
@@ -208,7 +208,7 @@ qt.style = "gtk2";
 
 It is possible to install custom [ XKB
 ](https://en.wikipedia.org/wiki/X_keyboard_extension) keyboard layouts
-using the option `services.xserver.extraLayouts`.
+using the option `services.xserver.xkb.extraLayouts`.
 
 As a first example, we are going to create a layout based on the basic
 US layout, with an additional layer to type some greek symbols by
@@ -235,7 +235,7 @@ xkb_symbols "us-greek"
 A minimal layout specification must include the following:
 
 ```nix
-services.xserver.extraLayouts.us-greek = {
+services.xserver.xkb.extraLayouts.us-greek = {
   description = "US layout with alt-gr greek";
   languages   = [ "eng" ];
   symbolsFile = /yourpath/symbols/us-greek;
@@ -298,7 +298,7 @@ xkb_symbols "media"
 As before, to install the layout do
 
 ```nix
-services.xserver.extraLayouts.media = {
+services.xserver.xkb.extraLayouts.media = {
   description  = "Multimedia keys remapping";
   languages    = [ "eng" ];
   symbolsFile  = /path/to/media-key;
diff --git a/nixpkgs/nixos/doc/manual/release-notes/rl-2311.section.md b/nixpkgs/nixos/doc/manual/release-notes/rl-2311.section.md
index ffe977bf33b1..c5406e183c39 100644
--- a/nixpkgs/nixos/doc/manual/release-notes/rl-2311.section.md
+++ b/nixpkgs/nixos/doc/manual/release-notes/rl-2311.section.md
@@ -322,7 +322,7 @@
   order, or relying on `mkBefore` and `mkAfter`, but may impact users calling
   `mkOrder n` with n ≤ 400.
 
-- X keyboard extension (XKB) options have been reorganized into a single attribute set, `services.xserver.xkb`. Specifically, `services.xserver.layout` is now `services.xserver.xkb.layout`, `services.xserver.xkbModel` is now `services.xserver.xkb.model`, `services.xserver.xkbOptions` is now `services.xserver.xkb.options`, `services.xserver.xkbVariant` is now `services.xserver.xkb.variant`, and `services.xserver.xkbDir` is now `services.xserver.xkb.dir`.
+- X keyboard extension (XKB) options have been reorganized into a single attribute set, `services.xserver.xkb`. Specifically, `services.xserver.layout` is now `services.xserver.xkb.layout`, `services.xserver.extraLayouts` is now `services.xserver.xkb.extraLayouts`, `services.xserver.xkbModel` is now `services.xserver.xkb.model`, `services.xserver.xkbOptions` is now `services.xserver.xkb.options`, `services.xserver.xkbVariant` is now `services.xserver.xkb.variant`, and `services.xserver.xkbDir` is now `services.xserver.xkb.dir`.
 
 - `networking.networkmanager.firewallBackend` was removed as NixOS is now using iptables-nftables-compat even when using iptables, therefore Networkmanager now uses the nftables backend unconditionally.
 
@@ -337,6 +337,8 @@
 
 - The `services.mtr-exporter.target` has been removed in favor of `services.mtr-exporter.jobs` which allows specifying multiple targets.
 
+- `blender-with-packages` has been deprecated in favor of `blender.withPackages`, for example `blender.withPackages (ps: [ps.bpycv])`. It behaves similarly to `python3.withPackages`.
+
 - Setting `nixpkgs.config` options while providing an external `pkgs` instance will now raise an error instead of silently ignoring the options. NixOS modules no longer set `nixpkgs.config` to accomodate this. This specifically affects `services.locate`, `services.xserver.displayManager.lightdm.greeters.tiny` and `programs.firefox` NixOS modules. No manual intervention should be required in most cases, however, configurations relying on those modules affecting packages outside the system environment should switch to explicit overlays.
 
 - `service.borgmatic.settings.location` and `services.borgmatic.configurations.<name>.location` are deprecated, please move your options out of sections to the global scope.
@@ -390,6 +392,9 @@
 
 - `zfs` was updated from 2.1.x to 2.2.0, [enabling newer kernel support and adding new features](https://github.com/openzfs/zfs/releases/tag/zfs-2.2.0).
 
+- Elixir now defaults to version
+  [v1.15](https://elixir-lang.org/blog/2023/06/19/elixir-v1-15-0-released/).
+
 - A new option was added to the virtualisation module that enables specifying explicitly named network interfaces in QEMU VMs. The existing `virtualisation.vlans` is still supported for cases where the name of the network interface is irrelevant.
 
 - DocBook option documentation is no longer supported, all module documentation now uses markdown.
@@ -413,6 +418,8 @@
 
 - `services.hedgedoc` has been heavily refactored, reducing the amount of declared options in the module. Most of the options should still work without any changes. Some options have been deprecated, as they no longer have any effect. See [#244941](https://github.com/NixOS/nixpkgs/pull/244941) for more details.
 
+- The [services.woodpecker-server](#opt-services.woodpecker-server.environmentFile) type was changed to list of paths to be more consistent to the woodpecker-agent module
+
 - The module [services.ankisyncd](#opt-services.ankisyncd.package) has been switched to [anki-sync-server-rs](https://github.com/ankicommunity/anki-sync-server-rs) from the old python version, which was difficult to update, had not been updated in a while, and did not support recent versions of anki.
 Unfortunately all servers supporting new clients (newer version of anki-sync-server, anki's built in sync server and this new rust package) do not support the older sync protocol that was used in the old server, so such old clients will also need updating and in particular the anki package in nixpkgs is also being updated in this release.
 The module update takes care of the new config syntax and the data itself (user login and cards) are compatible, so users of the module will be able to just log in again after updating both client and server without any extra action.
diff --git a/nixpkgs/nixos/modules/installer/cd-dvd/channel.nix b/nixpkgs/nixos/modules/installer/cd-dvd/channel.nix
index 8426ba8fac00..bc70dc985fe0 100644
--- a/nixpkgs/nixos/modules/installer/cd-dvd/channel.nix
+++ b/nixpkgs/nixos/modules/installer/cd-dvd/channel.nix
@@ -3,8 +3,6 @@
 
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   # This is copied into the installer image, so it's important that it is filtered
   # to avoid including a large .git directory.
@@ -27,38 +25,40 @@ let
       if [ ! -e $out/nixos/nixpkgs ]; then
         ln -s . $out/nixos/nixpkgs
       fi
-      ${optionalString (config.system.nixos.revision != null) ''
+      ${lib.optionalString (config.system.nixos.revision != null) ''
         echo -n ${config.system.nixos.revision} > $out/nixos/.git-revision
       ''}
       echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
       echo ${config.system.nixos.versionSuffix} | sed -e s/pre// > $out/nixos/svn-revision
     '';
-
 in
 
 {
-  # Pin the nixpkgs flake in the installer to our cleaned up nixpkgs source.
-  # FIXME: this might be surprising and is really only needed for offline installations,
-  # see discussion in https://github.com/NixOS/nixpkgs/pull/204178#issuecomment-1336289021
-  nix.registry.nixpkgs.to = {
-    type = "path";
-    path = "${channelSources}/nixos";
-  };
+  options.system.installer.channel.enable = (lib.mkEnableOption "bundling NixOS/Nixpkgs channel in the installer") // { default = true; };
+  config = lib.mkIf config.system.installer.channel.enable {
+    # Pin the nixpkgs flake in the installer to our cleaned up nixpkgs source.
+    # FIXME: this might be surprising and is really only needed for offline installations,
+    # see discussion in https://github.com/NixOS/nixpkgs/pull/204178#issuecomment-1336289021
+    nix.registry.nixpkgs.to = {
+      type = "path";
+      path = "${channelSources}/nixos";
+    };
 
-  # Provide the NixOS/Nixpkgs sources in /etc/nixos.  This is required
-  # for nixos-install.
-  boot.postBootCommands = mkAfter
-    ''
-      if ! [ -e /var/lib/nixos/did-channel-init ]; then
-        echo "unpacking the NixOS/Nixpkgs sources..."
-        mkdir -p /nix/var/nix/profiles/per-user/root
-        ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/per-user/root/channels \
-          -i ${channelSources} --quiet --option build-use-substitutes false \
-          ${optionalString config.boot.initrd.systemd.enable "--option sandbox false"} # There's an issue with pivot_root
-        mkdir -m 0700 -p /root/.nix-defexpr
-        ln -s /nix/var/nix/profiles/per-user/root/channels /root/.nix-defexpr/channels
-        mkdir -m 0755 -p /var/lib/nixos
-        touch /var/lib/nixos/did-channel-init
-      fi
-    '';
+    # Provide the NixOS/Nixpkgs sources in /etc/nixos.  This is required
+    # for nixos-install.
+    boot.postBootCommands = lib.mkAfter
+      ''
+        if ! [ -e /var/lib/nixos/did-channel-init ]; then
+          echo "unpacking the NixOS/Nixpkgs sources..."
+          mkdir -p /nix/var/nix/profiles/per-user/root
+          ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/per-user/root/channels \
+            -i ${channelSources} --quiet --option build-use-substitutes false \
+            ${lib.optionalString config.boot.initrd.systemd.enable "--option sandbox false"} # There's an issue with pivot_root
+          mkdir -m 0700 -p /root/.nix-defexpr
+          ln -s /nix/var/nix/profiles/per-user/root/channels /root/.nix-defexpr/channels
+          mkdir -m 0755 -p /var/lib/nixos
+          touch /var/lib/nixos/did-channel-init
+        fi
+      '';
+  };
 }
diff --git a/nixpkgs/nixos/modules/module-list.nix b/nixpkgs/nixos/modules/module-list.nix
index 92b49036a0de..5af65957f1bb 100644
--- a/nixpkgs/nixos/modules/module-list.nix
+++ b/nixpkgs/nixos/modules/module-list.nix
@@ -267,6 +267,7 @@
   ./programs/udevil.nix
   ./programs/usbtop.nix
   ./programs/vim.nix
+  ./programs/virt-manager.nix
   ./programs/wavemon.nix
   ./programs/wayland/cardboard.nix
   ./programs/wayland/river.nix
@@ -1507,6 +1508,7 @@
   ./virtualisation/docker.nix
   ./virtualisation/ecs-agent.nix
   ./virtualisation/hyperv-guest.nix
+  ./virtualisation/incus.nix
   ./virtualisation/kvmgt.nix
   ./virtualisation/libvirtd.nix
   ./virtualisation/lxc.nix
diff --git a/nixpkgs/nixos/modules/programs/firefox.nix b/nixpkgs/nixos/modules/programs/firefox.nix
index 85f47530cf5a..1edf935d1649 100644
--- a/nixpkgs/nixos/modules/programs/firefox.nix
+++ b/nixpkgs/nixos/modules/programs/firefox.nix
@@ -90,7 +90,7 @@ in
       description = mdDoc ''
         Group policies to install.
 
-        See [Mozilla's documentation](https://github.com/mozilla/policy-templates/blob/master/README.md)
+        See [Mozilla's documentation](https://mozilla.github.io/policy-templates/)
         for a list of available options.
 
         This can be used to install extensions declaratively! Check out the
diff --git a/nixpkgs/nixos/modules/security/sudo.nix b/nixpkgs/nixos/modules/security/sudo.nix
index d225442773c6..c665c15242a5 100644
--- a/nixpkgs/nixos/modules/security/sudo.nix
+++ b/nixpkgs/nixos/modules/security/sudo.nix
@@ -6,8 +6,6 @@ let
 
   cfg = config.security.sudo;
 
-  inherit (pkgs) sudo;
-
   toUserString = user: if (isInt user) then "#${toString user}" else "${user}";
   toGroupString = group: if (isInt group) then "%#${toString group}" else "%${group}";
 
@@ -247,7 +245,7 @@ in
       };
     };
 
-    environment.systemPackages = [ sudo ];
+    environment.systemPackages = [ cfg.package ];
 
     security.pam.services.sudo = { sshAgentAuth = true; usshAuth = true; };
 
diff --git a/nixpkgs/nixos/modules/services/continuous-integration/woodpecker/server.nix b/nixpkgs/nixos/modules/services/continuous-integration/woodpecker/server.nix
index cae5ed7cf116..38b42f7288c0 100644
--- a/nixpkgs/nixos/modules/services/continuous-integration/woodpecker/server.nix
+++ b/nixpkgs/nixos/modules/services/continuous-integration/woodpecker/server.nix
@@ -31,9 +31,9 @@ in
         description = lib.mdDoc "woodpecker-server config environment variables, for other options read the [documentation](https://woodpecker-ci.org/docs/administration/server-config)";
       };
       environmentFile = lib.mkOption {
-        type = lib.types.nullOr lib.types.path;
-        default = null;
-        example = "/root/woodpecker-server.env";
+        type = with lib.types; coercedTo path (f: [ f ]) (listOf path);
+        default = [ ];
+        example = [ "/root/woodpecker-server.env" ];
         description = lib.mdDoc ''
           File to load environment variables
           from. This is helpful for specifying secrets.
@@ -61,7 +61,7 @@ in
           StateDirectoryMode = "0700";
           UMask = "0007";
           ConfigurationDirectory = "woodpecker-server";
-          EnvironmentFile = lib.optional (cfg.environmentFile != null) cfg.environmentFile;
+          EnvironmentFile = cfg.environmentFile;
           ExecStart = "${cfg.package}/bin/woodpecker-server";
           Restart = "on-failure";
           RestartSec = 15;
diff --git a/nixpkgs/nixos/modules/services/hardware/fwupd.nix b/nixpkgs/nixos/modules/services/hardware/fwupd.nix
index 4e5913fd2751..7a938459d0cb 100644
--- a/nixpkgs/nixos/modules/services/hardware/fwupd.nix
+++ b/nixpkgs/nixos/modules/services/hardware/fwupd.nix
@@ -181,7 +181,18 @@ in {
     # required to update the firmware of disks
     services.udisks2.enable = true;
 
-    systemd.packages = [ cfg.package ];
+    systemd = {
+      packages = [ cfg.package ];
+
+      # fwupd-refresh expects a user that we do not create, so just run with DynamicUser
+      # instead and ensure we take ownership of /var/lib/fwupd
+      services.fwupd-refresh.serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "fwupd";
+      };
+
+      timers.fwupd-refresh.wantedBy = [ "timers.target" ];
+    };
 
     security.polkit.enable = true;
   };
diff --git a/nixpkgs/nixos/modules/services/hardware/throttled.nix b/nixpkgs/nixos/modules/services/hardware/throttled.nix
index 9fa495886119..0f1f00348ee8 100644
--- a/nixpkgs/nixos/modules/services/hardware/throttled.nix
+++ b/nixpkgs/nixos/modules/services/hardware/throttled.nix
@@ -27,6 +27,7 @@ in {
       then pkgs.writeText "throttled.conf" cfg.extraConfig
       else "${pkgs.throttled}/etc/throttled.conf";
 
+    hardware.cpu.x86.msr.enable = true;
     # Kernel 5.9 spams warnings whenever userspace writes to CPU MSRs.
     # See https://github.com/erpalma/throttled/issues/215
     hardware.cpu.x86.msr.settings.allow-writes =
diff --git a/nixpkgs/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixpkgs/nixos/modules/services/monitoring/grafana-image-renderer.nix
index 36258866646a..afe9eb4d7b95 100644
--- a/nixpkgs/nixos/modules/services/monitoring/grafana-image-renderer.nix
+++ b/nixpkgs/nixos/modules/services/monitoring/grafana-image-renderer.nix
@@ -108,7 +108,7 @@ in {
 
     services.grafana.settings.rendering = mkIf cfg.provisionGrafana {
       server_url = "http://localhost:${toString cfg.settings.service.port}/render";
-      callback_url = "http://localhost:${toString config.services.grafana.settings.server.http_port}";
+      callback_url = "http://${config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}";
     };
 
     services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
diff --git a/nixpkgs/nixos/modules/services/web-apps/akkoma.nix b/nixpkgs/nixos/modules/services/web-apps/akkoma.nix
index eaee70c712bb..5f9bbbd66374 100644
--- a/nixpkgs/nixos/modules/services/web-apps/akkoma.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/akkoma.nix
@@ -86,7 +86,7 @@ let
   # Erlang/Elixir uses a somewhat special format for IP addresses
   erlAddr = addr: fileContents
     (pkgs.runCommand addr {
-      nativeBuildInputs = with pkgs; [ elixir ];
+      nativeBuildInputs = [ cfg.package.elixirPackage ];
       code = ''
         case :inet.parse_address('${addr}') do
           {:ok, addr} -> IO.inspect addr
@@ -96,7 +96,7 @@ let
       passAsFile = [ "code" ];
     } ''elixir "$codePath" >"$out"'');
 
-  format = pkgs.formats.elixirConf { };
+  format = pkgs.formats.elixirConf { elixir = cfg.package.elixirPackage; };
   configFile = format.generate "config.exs"
     (replaceSec
       (attrsets.updateManyAttrsByPath [{
@@ -146,7 +146,7 @@ let
 
   initSecretsScript = writeShell {
     name = "akkoma-init-secrets";
-    runtimeInputs = with pkgs; [ coreutils elixir ];
+    runtimeInputs = with pkgs; [ coreutils cfg.package.elixirPackage ];
     text = let
       key-base = web.secret_key_base;
       jwt-signer = ex.":joken".":default_signer";
diff --git a/nixpkgs/nixos/modules/services/web-apps/freshrss.nix b/nixpkgs/nixos/modules/services/web-apps/freshrss.nix
index ffc05d0e41f8..8b4ea2aa53c9 100644
--- a/nixpkgs/nixos/modules/services/web-apps/freshrss.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/freshrss.nix
@@ -220,7 +220,7 @@ in
             "catch_workers_output" = true;
           };
           phpEnv = {
-            FRESHRSS_DATA_PATH = "${cfg.dataDir}";
+            DATA_PATH = "${cfg.dataDir}";
           };
         };
       };
@@ -267,7 +267,7 @@ in
             WorkingDirectory = cfg.package;
           };
           environment = {
-            FRESHRSS_DATA_PATH = cfg.dataDir;
+            DATA_PATH = cfg.dataDir;
           };
 
           script =
@@ -302,7 +302,7 @@ in
         wantedBy = [ "multi-user.target" ];
         startAt = "*:0/5";
         environment = {
-          FRESHRSS_DATA_PATH = cfg.dataDir;
+          DATA_PATH = cfg.dataDir;
         };
         serviceConfig = defaultServiceConfig //{
           ExecStart = "${cfg.package}/app/actualize_script.php";
diff --git a/nixpkgs/nixos/modules/services/web-apps/mobilizon.nix b/nixpkgs/nixos/modules/services/web-apps/mobilizon.nix
index e9264a38f0e6..343c5cead2b1 100644
--- a/nixpkgs/nixos/modules/services/web-apps/mobilizon.nix
+++ b/nixpkgs/nixos/modules/services/web-apps/mobilizon.nix
@@ -8,7 +8,7 @@ let
   user = "mobilizon";
   group = "mobilizon";
 
-  settingsFormat = pkgs.formats.elixirConf { elixir = pkgs.elixir_1_14; };
+  settingsFormat = pkgs.formats.elixirConf { elixir = cfg.package.elixirPackage; };
 
   configFile = settingsFormat.generate "mobilizon-config.exs" cfg.settings;
 
@@ -309,7 +309,7 @@ in
           genCookie = "IO.puts(Base.encode32(:crypto.strong_rand_bytes(32)))";
 
           evalElixir = str: ''
-            ${pkgs.elixir_1_14}/bin/elixir --eval '${str}'
+            ${cfg.package.elixirPackage}/bin/elixir --eval '${str}'
           '';
         in
         ''
diff --git a/nixpkgs/nixos/modules/services/x11/extra-layouts.nix b/nixpkgs/nixos/modules/services/x11/extra-layouts.nix
index 3941f50b7550..ab7e39739eeb 100644
--- a/nixpkgs/nixos/modules/services/x11/extra-layouts.nix
+++ b/nixpkgs/nixos/modules/services/x11/extra-layouts.nix
@@ -3,7 +3,7 @@
 with lib;
 
 let
-  layouts = config.services.xserver.extraLayouts;
+  layouts = config.services.xserver.xkb.extraLayouts;
 
   layoutOpts = {
     options = {
@@ -15,10 +15,10 @@ let
       languages = mkOption {
         type = types.listOf types.str;
         description =
-        lib.mdDoc ''
-          A list of languages provided by the layout.
-          (Use ISO 639-2 codes, for example: "eng" for english)
-        '';
+          lib.mdDoc ''
+            A list of languages provided by the layout.
+            (Use ISO 639-2 codes, for example: "eng" for english)
+          '';
       };
 
       compatFile = mkOption {
@@ -80,29 +80,37 @@ let
   };
 
   xkb_patched = pkgs.xorg.xkeyboardconfig_custom {
-    layouts = config.services.xserver.extraLayouts;
+    layouts = config.services.xserver.xkb.extraLayouts;
   };
 
 in
 
 {
 
+  imports = [
+    (lib.mkRenamedOptionModuleWith {
+      sinceRelease = 2311;
+      from = [ "services" "xserver" "extraLayouts" ];
+      to = [ "services" "xserver" "xkb" "extraLayouts" ];
+    })
+  ];
+
   ###### interface
 
-  options.services.xserver = {
+  options.services.xserver.xkb = {
     extraLayouts = mkOption {
       type = types.attrsOf (types.submodule layoutOpts);
-      default = {};
+      default = { };
       example = literalExpression
-      ''
-        {
-          mine = {
-            description = "My custom xkb layout.";
-            languages = [ "eng" ];
-            symbolsFile = /path/to/my/layout;
-          };
-        }
-      '';
+        ''
+          {
+            mine = {
+              description = "My custom xkb layout.";
+              languages = [ "eng" ];
+              symbolsFile = /path/to/my/layout;
+            };
+          }
+        '';
       description = lib.mdDoc ''
         Extra custom layouts that will be included in the xkb configuration.
         Information on how to create a new layout can be found here:
diff --git a/nixpkgs/nixos/modules/virtualisation/incus.nix b/nixpkgs/nixos/modules/virtualisation/incus.nix
new file mode 100644
index 000000000000..3a4f0d7157a0
--- /dev/null
+++ b/nixpkgs/nixos/modules/virtualisation/incus.nix
@@ -0,0 +1,236 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.virtualisation.incus;
+  preseedFormat = pkgs.formats.yaml { };
+in
+{
+  meta.maintainers = [ lib.maintainers.adamcstephens ];
+
+  options = {
+    virtualisation.incus = {
+      enable = lib.mkEnableOption (lib.mdDoc ''
+        incusd, a daemon that manages containers and virtual machines.
+
+        Users in the "incus-admin" group can interact with
+        the daemon (e.g. to start or stop containers) using the
+        {command}`incus` command line tool, among others.
+      '');
+
+      package = lib.mkPackageOptionMD pkgs "incus" { };
+
+      lxcPackage = lib.mkPackageOptionMD pkgs "lxc" { };
+
+      preseed = lib.mkOption {
+        type = lib.types.nullOr (
+          lib.types.submodule { freeformType = preseedFormat.type; }
+        );
+
+        default = null;
+
+        description = lib.mdDoc ''
+          Configuration for Incus preseed, see
+          <https://linuxcontainers.org/incus/docs/main/howto/initialize/#non-interactive-configuration>
+          for supported values.
+
+          Changes to this will be re-applied to Incus which will overwrite existing entities or create missing ones,
+          but entities will *not* be removed by preseed.
+        '';
+
+        example = {
+          networks = [
+            {
+              name = "incusbr0";
+              type = "bridge";
+              config = {
+                "ipv4.address" = "10.0.100.1/24";
+                "ipv4.nat" = "true";
+              };
+            }
+          ];
+          profiles = [
+            {
+              name = "default";
+              devices = {
+                eth0 = {
+                  name = "eth0";
+                  network = "incusbr0";
+                  type = "nic";
+                };
+                root = {
+                  path = "/";
+                  pool = "default";
+                  size = "35GiB";
+                  type = "disk";
+                };
+              };
+            }
+          ];
+          storage_pools = [
+            {
+              name = "default";
+              driver = "dir";
+              config = {
+                source = "/var/lib/incus/storage-pools/default";
+              };
+            }
+          ];
+        };
+      };
+
+      socketActivation = lib.mkEnableOption (
+        lib.mdDoc ''
+          socket-activation for starting incus.service. Enabling this option
+          will stop incus.service from starting automatically on boot.
+        ''
+      );
+
+      startTimeout = lib.mkOption {
+        type = lib.types.ints.unsigned;
+        default = 600;
+        apply = toString;
+        description = lib.mdDoc ''
+          Time to wait (in seconds) for incusd to become ready to process requests.
+          If incusd does not reply within the configured time, `incus.service` will be
+          considered failed and systemd will attempt to restart it.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    # https://github.com/lxc/incus/blob/f145309929f849b9951658ad2ba3b8f10cbe69d1/doc/reference/server_settings.md
+    boot.kernel.sysctl = {
+      "fs.aio-max-nr" = lib.mkDefault 524288;
+      "fs.inotify.max_queued_events" = lib.mkDefault 1048576;
+      "fs.inotify.max_user_instances" = lib.mkOverride 1050 1048576; # override in case conflict nixos/modules/services/x11/xserver.nix
+      "fs.inotify.max_user_watches" = lib.mkOverride 1050 1048576; # override in case conflict nixos/modules/services/x11/xserver.nix
+      "kernel.dmesg_restrict" = lib.mkDefault 1;
+      "kernel.keys.maxbytes" = lib.mkDefault 2000000;
+      "kernel.keys.maxkeys" = lib.mkDefault 2000;
+      "net.core.bpf_jit_limit" = lib.mkDefault 1000000000;
+      "net.ipv4.neigh.default.gc_thresh3" = lib.mkDefault 8192;
+      "net.ipv6.neigh.default.gc_thresh3" = lib.mkDefault 8192;
+      # vm.max_map_count is set higher in nixos/modules/config/sysctl.nix
+    };
+
+    boot.kernelModules = [
+      "veth"
+      "xt_comment"
+      "xt_CHECKSUM"
+      "xt_MASQUERADE"
+      "vhost_vsock"
+    ] ++ lib.optionals (!config.networking.nftables.enable) [ "iptable_mangle" ];
+
+    environment.systemPackages = [ cfg.package ];
+
+    # Note: the following options are also declared in virtualisation.lxc, but
+    # the latter can't be simply enabled to reuse the formers, because it
+    # does a bunch of unrelated things.
+    systemd.tmpfiles.rules = [ "d /var/lib/lxc/rootfs 0755 root root -" ];
+
+    security.apparmor = {
+      packages = [ cfg.lxcPackage ];
+      policies = {
+        "bin.lxc-start".profile = ''
+          include ${cfg.lxcPackage}/etc/apparmor.d/usr.bin.lxc-start
+        '';
+        "lxc-containers".profile = ''
+          include ${cfg.lxcPackage}/etc/apparmor.d/lxc-containers
+        '';
+      };
+    };
+
+    systemd.services.incus = {
+      description = "Incus Container and Virtual Machine Management Daemon";
+
+      wantedBy = lib.mkIf (!cfg.socketActivation) [ "multi-user.target" ];
+      after = [
+        "network-online.target"
+        "lxcfs.service"
+      ] ++ (lib.optional cfg.socketActivation "incus.socket");
+      requires = [
+        "lxcfs.service"
+      ] ++ (lib.optional cfg.socketActivation "incus.socket");
+      wants = [
+        "network-online.target"
+      ];
+
+      path = lib.mkIf config.boot.zfs.enabled [ config.boot.zfs.package ];
+
+      environment = {
+        # Override Path to the LXC template configuration directory
+        INCUS_LXC_TEMPLATE_CONFIG = "${pkgs.lxcfs}/share/lxc/config";
+      };
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/incusd --group incus-admin";
+        ExecStartPost = "${cfg.package}/bin/incusd waitready --timeout=${cfg.startTimeout}";
+        ExecStop = "${cfg.package}/bin/incus admin shutdown";
+
+        KillMode = "process"; # when stopping, leave the containers alone
+        Delegate = "yes";
+        LimitMEMLOCK = "infinity";
+        LimitNOFILE = "1048576";
+        LimitNPROC = "infinity";
+        TasksMax = "infinity";
+
+        Restart = "on-failure";
+        TimeoutStartSec = "${cfg.startTimeout}s";
+        TimeoutStopSec = "30s";
+      };
+    };
+
+    systemd.sockets.incus = lib.mkIf cfg.socketActivation {
+      description = "Incus UNIX socket";
+      wantedBy = [ "sockets.target" ];
+
+      socketConfig = {
+        ListenStream = "/var/lib/incus/unix.socket";
+        SocketMode = "0660";
+        SocketGroup = "incus-admin";
+        Service = "incus.service";
+      };
+    };
+
+    systemd.services.incus-preseed = lib.mkIf (cfg.preseed != null) {
+      description = "Incus initialization with preseed file";
+
+      wantedBy = ["incus.service"];
+      after = ["incus.service"];
+      bindsTo = ["incus.service"];
+      partOf = ["incus.service"];
+
+      script = ''
+        ${cfg.package}/bin/incus admin init --preseed <${
+          preseedFormat.generate "incus-preseed.yaml" cfg.preseed
+        }
+      '';
+
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
+    };
+
+    users.groups.incus-admin = { };
+
+    users.users.root = {
+      # match documented default ranges https://linuxcontainers.org/incus/docs/main/userns-idmap/#allowed-ranges
+      subUidRanges = [
+        {
+          startUid = 1000000;
+          count = 1000000000;
+        }
+      ];
+      subGidRanges = [
+        {
+          startGid = 1000000;
+          count = 1000000000;
+        }
+      ];
+    };
+
+    virtualisation.lxc.lxcfs.enable = true;
+  };
+}
diff --git a/nixpkgs/nixos/tests/all-tests.nix b/nixpkgs/nixos/tests/all-tests.nix
index f7f8ac8fec88..6201045b54cc 100644
--- a/nixpkgs/nixos/tests/all-tests.nix
+++ b/nixpkgs/nixos/tests/all-tests.nix
@@ -394,6 +394,7 @@ in {
   icingaweb2 = handleTest ./icingaweb2.nix {};
   iftop = handleTest ./iftop.nix {};
   incron = handleTest ./incron.nix {};
+  incus = pkgs.recurseIntoAttrs (handleTest ./incus { inherit handleTestOn; });
   influxdb = handleTest ./influxdb.nix {};
   influxdb2 = handleTest ./influxdb2.nix {};
   initrd-network-openvpn = handleTest ./initrd-network-openvpn {};
@@ -762,6 +763,7 @@ in {
   spark = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./spark {};
   sqlite3-to-mysql = handleTest ./sqlite3-to-mysql.nix {};
   sslh = handleTest ./sslh.nix {};
+  ssh-audit = handleTest ./ssh-audit.nix {};
   sssd = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./sssd.nix {};
   sssd-ldap = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./sssd-ldap.nix {};
   stalwart-mail = handleTest ./stalwart-mail.nix {};
diff --git a/nixpkgs/nixos/tests/incus/container.nix b/nixpkgs/nixos/tests/incus/container.nix
new file mode 100644
index 000000000000..79b9e2fbabdc
--- /dev/null
+++ b/nixpkgs/nixos/tests/incus/container.nix
@@ -0,0 +1,77 @@
+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;
+    };
+  };
+
+  container-image-metadata = releases.lxdContainerMeta.${pkgs.stdenv.hostPlatform.system};
+  container-image-rootfs = releases.lxdContainerImage.${pkgs.stdenv.hostPlatform.system};
+in
+{
+  name = "incus-container";
+
+  meta.maintainers = with lib.maintainers; [ adamcstephens ];
+
+  nodes.machine = { ... }: {
+    virtualisation = {
+      # Ensure test VM has enough resources for creating and managing guests
+      cores = 2;
+      memorySize = 1024;
+      diskSize = 4096;
+
+      incus.enable = true;
+    };
+  };
+
+  testScript = ''
+    def instance_is_up(_) -> bool:
+        status, _ = machine.execute("incus exec container --disable-stdin --force-interactive /run/current-system/sw/bin/true")
+        return status == 0
+
+    def set_container(config):
+        machine.succeed(f"incus config set container {config}")
+        machine.succeed("incus restart container")
+        with machine.nested("Waiting for instance to start and be usable"):
+          retry(instance_is_up)
+
+    machine.wait_for_unit("incus.service")
+
+    # no preseed should mean no service
+    machine.fail("systemctl status incus-preseed.service")
+
+    machine.succeed("incus admin init --minimal")
+
+    with subtest("Container image can be imported"):
+        machine.succeed("incus image import ${container-image-metadata}/*/*.tar.xz ${container-image-rootfs}/*/*.tar.xz --alias nixos")
+
+    with subtest("Container can be launched and managed"):
+        machine.succeed("incus launch nixos container")
+        with machine.nested("Waiting for instance to start and be usable"):
+          retry(instance_is_up)
+        machine.succeed("echo true | incus exec container /run/current-system/sw/bin/bash -")
+
+    with subtest("Container CPU limits can be managed"):
+        set_container("limits.cpu 1")
+        cpuinfo = machine.succeed("incus exec container grep -- -c ^processor /proc/cpuinfo").strip()
+        assert cpuinfo == "1", f"Wrong number of CPUs reported from /proc/cpuinfo, want: 1, got: {cpuinfo}"
+
+        set_container("limits.cpu 2")
+        cpuinfo = machine.succeed("incus exec container grep -- -c ^processor /proc/cpuinfo").strip()
+        assert cpuinfo == "2", f"Wrong number of CPUs reported from /proc/cpuinfo, want: 2, got: {cpuinfo}"
+
+    with subtest("Container memory limits can be managed"):
+        set_container("limits.memory 64MB")
+        meminfo = machine.succeed("incus exec container grep -- MemTotal /proc/meminfo").strip()
+        meminfo_bytes = " ".join(meminfo.split(' ')[-2:])
+        assert meminfo_bytes == "62500 kB", f"Wrong amount of memory reported from /proc/meminfo, want: '62500 kB', got: '{meminfo_bytes}'"
+
+        set_container("limits.memory 128MB")
+        meminfo = machine.succeed("incus exec container grep -- MemTotal /proc/meminfo").strip()
+        meminfo_bytes = " ".join(meminfo.split(' ')[-2:])
+        assert meminfo_bytes == "125000 kB", f"Wrong amount of memory reported from /proc/meminfo, want: '125000 kB', got: '{meminfo_bytes}'"
+  '';
+})
diff --git a/nixpkgs/nixos/tests/incus/default.nix b/nixpkgs/nixos/tests/incus/default.nix
new file mode 100644
index 000000000000..c88974605e30
--- /dev/null
+++ b/nixpkgs/nixos/tests/incus/default.nix
@@ -0,0 +1,14 @@
+{
+  system ? builtins.currentSystem,
+  config ? { },
+  pkgs ? import ../../.. { inherit system config; },
+  handleTestOn,
+}:
+{
+  container = import ./container.nix { inherit system pkgs; };
+  preseed = import ./preseed.nix { inherit system pkgs; };
+  socket-activated = import ./socket-activated.nix { inherit system pkgs; };
+  virtual-machine = handleTestOn [ "x86_64-linux" ] ./virtual-machine.nix {
+    inherit system pkgs;
+  };
+}
diff --git a/nixpkgs/nixos/tests/incus/preseed.nix b/nixpkgs/nixos/tests/incus/preseed.nix
new file mode 100644
index 000000000000..47b2d0cd6228
--- /dev/null
+++ b/nixpkgs/nixos/tests/incus/preseed.nix
@@ -0,0 +1,60 @@
+import ../make-test-python.nix ({ pkgs, lib, ... } :
+
+{
+  name = "incus-preseed";
+
+  meta.maintainers = with lib.maintainers; [ adamcstephens ];
+
+  nodes.machine = { lib, ... }: {
+    virtualisation = {
+      incus.enable = true;
+
+      incus.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 = ''
+    machine.wait_for_unit("incus.service")
+    machine.wait_for_unit("incus-preseed.service")
+
+    with subtest("Verify preseed resources created"):
+      machine.succeed("incus profile show nixostest_default")
+      machine.succeed("incus network info nixostestbr0")
+      machine.succeed("incus storage show nixostest_pool")
+  '';
+})
diff --git a/nixpkgs/nixos/tests/incus/socket-activated.nix b/nixpkgs/nixos/tests/incus/socket-activated.nix
new file mode 100644
index 000000000000..4d25b26a15f5
--- /dev/null
+++ b/nixpkgs/nixos/tests/incus/socket-activated.nix
@@ -0,0 +1,26 @@
+import ../make-test-python.nix ({ pkgs, lib, ... } :
+
+{
+  name = "incus-socket-activated";
+
+  meta.maintainers = with lib.maintainers; [ adamcstephens ];
+
+  nodes.machine = { lib, ... }: {
+    virtualisation = {
+      incus.enable = true;
+      incus.socketActivation = true;
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("incus.socket")
+
+    # ensure service is not running by default
+    machine.fail("systemctl is-active incus.service")
+    machine.fail("systemctl is-active incus-preseed.service")
+
+    # access the socket and ensure the service starts
+    machine.succeed("incus list")
+    machine.wait_for_unit("incus.service")
+  '';
+})
diff --git a/nixpkgs/nixos/tests/incus/virtual-machine.nix b/nixpkgs/nixos/tests/incus/virtual-machine.nix
new file mode 100644
index 000000000000..bfa116679d43
--- /dev/null
+++ b/nixpkgs/nixos/tests/incus/virtual-machine.nix
@@ -0,0 +1,55 @@
+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];
+    };
+  };
+
+  vm-image-metadata = releases.lxdVirtualMachineImageMeta.${pkgs.stdenv.hostPlatform.system};
+  vm-image-disk = releases.lxdVirtualMachineImage.${pkgs.stdenv.hostPlatform.system};
+
+  instance-name = "instance1";
+in
+{
+  name = "incus-virtual-machine";
+
+  meta.maintainers = with lib.maintainers; [ adamcstephens ];
+
+  nodes.machine = {...}: {
+    virtualisation = {
+      # Ensure test VM has enough resources for creating and managing guests
+      cores = 2;
+      memorySize = 1024;
+      diskSize = 4096;
+
+      incus.enable = true;
+    };
+  };
+
+  testScript = ''
+    def instance_is_up(_) -> bool:
+      status, _ = machine.execute("incus exec ${instance-name} --disable-stdin --force-interactive /run/current-system/sw/bin/true")
+      return status == 0
+
+    machine.wait_for_unit("incus.service")
+
+    machine.succeed("incus admin init --minimal")
+
+    with subtest("virtual-machine image can be imported"):
+        machine.succeed("incus image import ${vm-image-metadata}/*/*.tar.xz ${vm-image-disk}/nixos.qcow2 --alias nixos")
+
+    with subtest("virtual-machine can be launched and become available"):
+        machine.succeed("incus 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("incus exec ${instance-name} systemctl is-active lxd-agent")
+  '';
+})
diff --git a/nixpkgs/nixos/tests/keymap.nix b/nixpkgs/nixos/tests/keymap.nix
index 0e160269304b..e8973a50f852 100644
--- a/nixpkgs/nixos/tests/keymap.nix
+++ b/nixpkgs/nixos/tests/keymap.nix
@@ -213,7 +213,7 @@ in pkgs.lib.mapAttrs mkKeyboardTest {
 
     extraConfig.console.useXkbConfig = true;
     extraConfig.services.xserver.xkb.layout = "us-greek";
-    extraConfig.services.xserver.extraLayouts.us-greek =
+    extraConfig.services.xserver.xkb.extraLayouts.us-greek =
       { description = "US layout with alt-gr greek";
         languages   = [ "eng" ];
         symbolsFile = pkgs.writeText "us-greek" ''
diff --git a/nixpkgs/nixos/tests/ssh-audit.nix b/nixpkgs/nixos/tests/ssh-audit.nix
new file mode 100644
index 000000000000..bd6255b8044d
--- /dev/null
+++ b/nixpkgs/nixos/tests/ssh-audit.nix
@@ -0,0 +1,103 @@
+import ./make-test-python.nix (
+  {pkgs, ...}: let
+    sshKeys = import (pkgs.path + "/nixos/tests/ssh-keys.nix") pkgs;
+    sshUsername = "any-user";
+    serverName = "server";
+    clientName = "client";
+    sshAuditPort = 2222;
+  in {
+    name = "ssh";
+
+    nodes = {
+      "${serverName}" = {
+        networking.firewall.allowedTCPPorts = [
+          sshAuditPort
+        ];
+        services.openssh.enable = true;
+        users.users."${sshUsername}" = {
+          isNormalUser = true;
+          openssh.authorizedKeys.keys = [
+            sshKeys.snakeOilPublicKey
+          ];
+        };
+      };
+      "${clientName}" = {
+        programs.ssh = {
+          ciphers = [
+            "aes128-ctr"
+            "aes128-gcm@openssh.com"
+            "aes192-ctr"
+            "aes256-ctr"
+            "aes256-gcm@openssh.com"
+            "chacha20-poly1305@openssh.com"
+          ];
+          extraConfig = ''
+            IdentitiesOnly yes
+          '';
+          hostKeyAlgorithms = [
+            "rsa-sha2-256"
+            "rsa-sha2-256-cert-v01@openssh.com"
+            "rsa-sha2-512"
+            "rsa-sha2-512-cert-v01@openssh.com"
+            "sk-ssh-ed25519-cert-v01@openssh.com"
+            "sk-ssh-ed25519@openssh.com"
+            "ssh-ed25519"
+            "ssh-ed25519-cert-v01@openssh.com"
+          ];
+          kexAlgorithms = [
+            "curve25519-sha256"
+            "curve25519-sha256@libssh.org"
+            "diffie-hellman-group-exchange-sha256"
+            "diffie-hellman-group16-sha512"
+            "diffie-hellman-group18-sha512"
+            "sntrup761x25519-sha512@openssh.com"
+          ];
+          macs = [
+            "hmac-sha2-256-etm@openssh.com"
+            "hmac-sha2-512-etm@openssh.com"
+            "umac-128-etm@openssh.com"
+          ];
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      ${serverName}.wait_for_open_port(22)
+
+      # Should pass SSH server audit
+      ${serverName}.succeed("${pkgs.ssh-audit}/bin/ssh-audit 127.0.0.1")
+
+      # Wait for client to be able to connect to the server
+      ${clientName}.wait_for_unit("network-online.target")
+
+      # Set up trusted private key
+      ${clientName}.succeed("cat ${sshKeys.snakeOilPrivateKey} > privkey.snakeoil")
+      ${clientName}.succeed("chmod 600 privkey.snakeoil")
+
+      # Fail fast and disable interactivity
+      ssh_options = "-o BatchMode=yes -o ConnectTimeout=1 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
+
+      # Should deny root user
+      ${clientName}.fail(f"ssh {ssh_options} root@${serverName} true")
+
+      # Should deny non-root user password login
+      ${clientName}.fail(f"ssh {ssh_options} -o PasswordAuthentication=yes ${sshUsername}@${serverName} true")
+
+      # Should allow non-root user certificate login
+      ${clientName}.succeed(f"ssh {ssh_options} -i privkey.snakeoil ${sshUsername}@${serverName} true")
+
+      # Should pass SSH client audit
+      service_name = "ssh-audit.service"
+      ${serverName}.succeed(f"systemd-run --unit={service_name} ${pkgs.ssh-audit}/bin/ssh-audit --client-audit --port=${toString sshAuditPort}")
+      ${clientName}.sleep(5) # We can't use wait_for_open_port because ssh-audit exits as soon as anything talks to it
+      ${clientName}.execute(
+          f"ssh {ssh_options} -i privkey.snakeoil -p ${toString sshAuditPort} ${sshUsername}@${serverName} true",
+          check_return=False,
+          timeout=10
+      )
+      ${serverName}.succeed(f"exit $(systemctl show --property=ExecMainStatus --value {service_name})")
+    '';
+  }
+)