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/release-notes/rl-2411.section.md10
-rw-r--r--nixos/lib/test-driver/default.nix13
-rw-r--r--nixos/modules/programs/ydotool.nix25
-rw-r--r--nixos/modules/services/audio/navidrome.nix2
-rw-r--r--nixos/modules/services/misc/jellyfin.nix2
-rw-r--r--nixos/modules/services/networking/aria2.nix7
-rw-r--r--nixos/modules/services/security/vaultwarden/default.nix47
-rw-r--r--nixos/modules/services/ttys/getty.nix3
-rw-r--r--nixos/modules/services/x11/window-managers/qtile.nix39
-rw-r--r--nixos/tests/all-tests.nix2
-rw-r--r--nixos/tests/firefly-iii.nix4
-rw-r--r--nixos/tests/qtile.nix2
-rw-r--r--nixos/tests/vaultwarden.nix271
-rw-r--r--nixos/tests/ydotool.nix257
14 files changed, 405 insertions, 279 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md
index 7777df071b182..889d399749323 100644
--- a/nixos/doc/manual/release-notes/rl-2411.section.md
+++ b/nixos/doc/manual/release-notes/rl-2411.section.md
@@ -19,6 +19,8 @@
 
 ## Backward Incompatibilities {#sec-release-24.11-incompatibilities}
 
+- `androidenv.androidPkgs_9_0` has been removed, and replaced with `androidenv.androidPkgs` for a more complete Android SDK including support for Android 9 and later.
+
 - `nginx` package no longer includes `gd` and `geoip` dependencies. For enabling it, override `nginx` package with the optionals `withImageFilter` and `withGeoIP`.
 
 - `openssh` and `openssh_hpn` are now compiled without Kerberos 5 / GSSAPI support in an effort to reduce the attack surface of the components for the majority of users. Users needing this support can
@@ -35,6 +37,10 @@
 
 - `services.ddclient.use` has been deprecated: `ddclient` now supports separate IPv4 and IPv6 configuration. Use `services.ddclient.usev4` and `services.ddclient.usev6` instead.
 
+- `vaultwarden` lost the capability to bind to privileged ports. If you rely on
+   this behavior, override the systemd unit to allow `CAP_NET_BIND_SERVICE` in
+   your local configuration.
+
 - The Invoiceplane module now only accepts the structured `settings` option.
   `extraConfig` is now removed.
 
@@ -67,6 +73,10 @@
   services.portunus.ldap.package = pkgs.openldap.override { libxcrypt = pkgs.libxcrypt-legacy; };
   ```
 
+- The `tracy` package no longer works on X11, since it's moved to Wayland
+  support, which is the intended default behavior by Tracy maintainers.
+  X11 users have to switch to the new package `tracy-x11`.
+
 ## Other Notable Changes {#sec-release-24.11-notable-changes}
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
diff --git a/nixos/lib/test-driver/default.nix b/nixos/lib/test-driver/default.nix
index 7a88694b3167e..26652db6016e6 100644
--- a/nixos/lib/test-driver/default.nix
+++ b/nixos/lib/test-driver/default.nix
@@ -13,11 +13,20 @@
 , extraPythonPackages ? (_ : [])
 , nixosTests
 }:
-
+let
+  fs = lib.fileset;
+in
 python3Packages.buildPythonApplication {
   pname = "nixos-test-driver";
   version = "1.1";
-  src = ./.;
+  src = fs.toSource {
+    root = ./.;
+    fileset = fs.unions [
+      ./pyproject.toml
+      ./test_driver
+      ./extract-docstrings.py
+    ];
+  };
   pyproject = true;
 
   propagatedBuildInputs = [
diff --git a/nixos/modules/programs/ydotool.nix b/nixos/modules/programs/ydotool.nix
index f639e9283de42..643a5d369f3fc 100644
--- a/nixos/modules/programs/ydotool.nix
+++ b/nixos/modules/programs/ydotool.nix
@@ -14,23 +14,32 @@ in
 
   options.programs.ydotool = {
     enable = lib.mkEnableOption ''
-      ydotoold system service and install ydotool.
-      Add yourself to the 'ydotool' group to be able to use it.
+      ydotoold system service and {command}`ydotool` for members of
+      {option}`programs.ydotool.group`.
     '';
+    group = lib.mkOption {
+      type = lib.types.str;
+      default = "ydotool";
+      description = ''
+        Group which users must be in to use {command}`ydotool`.
+      '';
+    };
   };
 
-  config = lib.mkIf cfg.enable {
-    users.groups.ydotool = { };
+  config = let
+    runtimeDirectory = "ydotoold";
+  in lib.mkIf cfg.enable {
+    users.groups."${config.programs.ydotool.group}" = { };
 
     systemd.services.ydotoold = {
       description = "ydotoold - backend for ydotool";
       wantedBy = [ "multi-user.target" ];
       partOf = [ "multi-user.target" ];
       serviceConfig = {
-        Group = "ydotool";
-        RuntimeDirectory = "ydotoold";
+        Group = config.programs.ydotool.group;
+        RuntimeDirectory = runtimeDirectory;
         RuntimeDirectoryMode = "0750";
-        ExecStart = "${lib.getExe' pkgs.ydotool "ydotoold"} --socket-path=/run/ydotoold/socket --socket-perm=0660";
+        ExecStart = "${lib.getExe' pkgs.ydotool "ydotoold"} --socket-path=${config.environment.variables.YDOTOOL_SOCKET} --socket-perm=0660";
 
         # hardening
 
@@ -76,7 +85,7 @@ in
     };
 
     environment.variables = {
-      YDOTOOL_SOCKET = "/run/ydotoold/socket";
+      YDOTOOL_SOCKET = "/run/${runtimeDirectory}/socket";
     };
     environment.systemPackages = with pkgs; [ ydotool ];
   };
diff --git a/nixos/modules/services/audio/navidrome.nix b/nixos/modules/services/audio/navidrome.nix
index a9db9228827a2..06d2d174a4df3 100644
--- a/nixos/modules/services/audio/navidrome.nix
+++ b/nixos/modules/services/audio/navidrome.nix
@@ -157,5 +157,5 @@ in
 
       networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.Port ];
     };
-  meta.maintainers = with maintainers; [ nu-nu-ko ];
+  meta.maintainers = with maintainers; [ fsnkty ];
 }
diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix
index a1d3910bd93b0..a006090878422 100644
--- a/nixos/modules/services/misc/jellyfin.nix
+++ b/nixos/modules/services/misc/jellyfin.nix
@@ -160,5 +160,5 @@ in
 
   };
 
-  meta.maintainers = with maintainers; [ minijackson nu-nu-ko ];
+  meta.maintainers = with maintainers; [ minijackson fsnkty ];
 }
diff --git a/nixos/modules/services/networking/aria2.nix b/nixos/modules/services/networking/aria2.nix
index dd4823911f2b3..f0d5c5c8a21e3 100644
--- a/nixos/modules/services/networking/aria2.nix
+++ b/nixos/modules/services/networking/aria2.nix
@@ -7,12 +7,6 @@ let
   defaultRpcListenPort = 6800;
   defaultDir = "${homeDir}/Downloads";
 
-  rangesToStringList = map (x:
-    if x.from == x.to
-    then builtins.toString x.from
-    else builtins.toString x.from + "-" + builtins.toString x.to
-  );
-
   portRangesToString = ranges: lib.concatStringsSep "," (map
     (x:
       if x.from == x.to
@@ -77,6 +71,7 @@ in
 
           [0]: https://aria2.github.io/manual/en/html/aria2c.html#synopsis
         '';
+        default = { };
         type = lib.types.submodule {
           freeformType = with lib.types; attrsOf (oneOf [ bool int float singleLineStr ]);
           options = {
diff --git a/nixos/modules/services/security/vaultwarden/default.nix b/nixos/modules/services/security/vaultwarden/default.nix
index 33957be437b30..41f7de5d80fab 100644
--- a/nixos/modules/services/security/vaultwarden/default.nix
+++ b/nixos/modules/services/security/vaultwarden/default.nix
@@ -5,6 +5,8 @@ let
   user = config.users.users.vaultwarden.name;
   group = config.users.groups.vaultwarden.name;
 
+  StateDirectory = if lib.versionOlder config.system.stateVersion "24.11" then "bitwarden_rs" else "vaultwarden";
+
   # Convert name from camel case (e.g. disable2FARemember) to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
   nameToEnvVar = name:
     let
@@ -23,7 +25,7 @@ let
       configEnv = lib.concatMapAttrs (name: value: lib.optionalAttrs (value != null) {
         ${nameToEnvVar name} = if lib.isBool value then lib.boolToString value else toString value;
       }) cfg.config;
-    in { DATA_FOLDER = "/var/lib/bitwarden_rs"; } // lib.optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
+    in { DATA_FOLDER = "/var/lib/${StateDirectory}"; } // lib.optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
       WEB_VAULT_FOLDER = "${cfg.webVaultPackage}/share/vaultwarden/vault";
     } // configEnv;
 
@@ -176,16 +178,45 @@ in {
         User = user;
         Group = group;
         EnvironmentFile = [ configFile ] ++ lib.optional (cfg.environmentFile != null) cfg.environmentFile;
-        ExecStart = "${vaultwarden}/bin/vaultwarden";
+        ExecStart = lib.getExe vaultwarden;
         LimitNOFILE = "1048576";
-        PrivateTmp = "true";
-        PrivateDevices = "true";
-        ProtectHome = "true";
+        CapabilityBoundingSet = [ "" ];
+        DeviceAllow = [ "" ];
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "noaccess";
         ProtectSystem = "strict";
-        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
-        StateDirectory = "bitwarden_rs";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_UNIX"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        inherit StateDirectory;
         StateDirectoryMode = "0700";
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+        ];
         Restart = "always";
+        UMask = "0077";
       };
       wantedBy = [ "multi-user.target" ];
     };
@@ -193,7 +224,7 @@ in {
     systemd.services.backup-vaultwarden = lib.mkIf (cfg.backupDir != null) {
       description = "Backup vaultwarden";
       environment = {
-        DATA_FOLDER = "/var/lib/bitwarden_rs";
+        DATA_FOLDER = "/var/lib/${StateDirectory}";
         BACKUP_FOLDER = cfg.backupDir;
       };
       path = with pkgs; [ sqlite ];
diff --git a/nixos/modules/services/ttys/getty.nix b/nixos/modules/services/ttys/getty.nix
index 011016dd5fd14..e88bb4628635e 100644
--- a/nixos/modules/services/ttys/getty.nix
+++ b/nixos/modules/services/ttys/getty.nix
@@ -101,7 +101,7 @@ in
   config = {
     # Note: this is set here rather than up there so that changing
     # nixos.label would not rebuild manual pages
-    services.getty.greetingLine = mkDefault ''<<< Welcome to NixOS ${config.system.nixos.label} (\m) - \l >>>'';
+    services.getty.greetingLine = mkDefault ''<<< Welcome to ${config.system.nixos.distroName} ${config.system.nixos.label} (\m) - \l >>>'';
     services.getty.helpLine = mkIf (config.documentation.nixos.enable && config.documentation.doc.enable) "\nRun 'nixos-help' for the NixOS manual.";
 
     systemd.services."getty@" =
@@ -158,4 +158,5 @@ in
 
   };
 
+  meta.maintainers = with maintainers; [ RossComputerGuy ];
 }
diff --git a/nixos/modules/services/x11/window-managers/qtile.nix b/nixos/modules/services/x11/window-managers/qtile.nix
index 700ead8366008..4603ca3fb50f0 100644
--- a/nixos/modules/services/x11/window-managers/qtile.nix
+++ b/nixos/modules/services/x11/window-managers/qtile.nix
@@ -7,6 +7,10 @@ let
 in
 
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "xserver" "windowManager" "qtile" "backend" ] "The qtile package now provides separate display sessions for both X11 and Wayland.")
+  ];
+
   options.services.xserver.windowManager.qtile = {
     enable = mkEnableOption "qtile";
 
@@ -22,14 +26,6 @@ in
       '';
     };
 
-    backend = mkOption {
-      type = types.enum [ "x11" "wayland" ];
-      default = "x11";
-      description = ''
-          Backend to use in qtile: `x11` or `wayland`.
-      '';
-    };
-
     extraPackages = mkOption {
         type = types.functionTo (types.listOf types.package);
         default = _: [];
@@ -57,25 +53,14 @@ in
   };
 
   config = mkIf cfg.enable {
-    services.xserver.windowManager.qtile.finalPackage = pkgs.python3.withPackages (p:
-      [ (cfg.package.unwrapped or cfg.package) ] ++ (cfg.extraPackages p)
-    );
-
-    services.xserver.windowManager.session = [{
-      name = "qtile";
-      start = ''
-        ${cfg.finalPackage}/bin/qtile start -b ${cfg.backend} \
-        ${optionalString (cfg.configFile != null)
-        "--config \"${cfg.configFile}\""} &
-        waitPID=$!
-      '';
-    }];
+    services = {
+      xserver.windowManager.qtile.finalPackage = pkgs.python3.pkgs.qtile.override { extraPackages = cfg.extraPackages pkgs.python3.pkgs; };
+      displayManager.sessionPackages = [ cfg.finalPackage ];
+    };
 
-    environment.systemPackages = [
-      # pkgs.qtile is currently a buildenv of qtile and its dependencies.
-      # For userland commands, we want the underlying package so that
-      # packages such as python don't bleed into userland and overwrite intended behavior.
-      (cfg.package.unwrapped or cfg.package)
-    ];
+    environment = {
+      etc."xdg/qtile/config.py" = mkIf (cfg.configFile != null) { source = cfg.configFile; };
+      systemPackages = [ cfg.finalPackage ];
+    };
   };
 }
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index a9b6881aab0f8..746b29fd27258 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -1019,7 +1019,7 @@ in {
   vault-agent = handleTest ./vault-agent.nix {};
   vault-dev = handleTest ./vault-dev.nix {};
   vault-postgresql = handleTest ./vault-postgresql.nix {};
-  vaultwarden = handleTest ./vaultwarden.nix {};
+  vaultwarden = discoverTests (import ./vaultwarden.nix);
   vector = handleTest ./vector {};
   vengi-tools = handleTest ./vengi-tools.nix {};
   victoriametrics = handleTest ./victoriametrics.nix {};
diff --git a/nixos/tests/firefly-iii.nix b/nixos/tests/firefly-iii.nix
index 2373ba8360264..f8e4ca4bfe2b4 100644
--- a/nixos/tests/firefly-iii.nix
+++ b/nixos/tests/firefly-iii.nix
@@ -39,12 +39,13 @@ in
         DB_DATABASE = "firefly";
         DB_USERNAME = "firefly";
         DB_PASSWORD_FILE = "/etc/postgres-pass";
+        PGSQL_SCHEMA = "firefly";
       };
     };
 
     services.postgresql = {
       enable = true;
-      package = pkgs.postgresql_15;
+      package = pkgs.postgresql_16;
       authentication = ''
         local all postgres peer
         local firefly firefly password
@@ -52,6 +53,7 @@ in
       initialScript = pkgs.writeText "firefly-init.sql" ''
         CREATE USER "firefly" WITH LOGIN PASSWORD '${db-pass}';
         CREATE DATABASE "firefly" WITH OWNER "firefly";
+        \c firefly
         CREATE SCHEMA AUTHORIZATION firefly;
       '';
     };
diff --git a/nixos/tests/qtile.nix b/nixos/tests/qtile.nix
index b4d8f9d421144..96afaa342c524 100644
--- a/nixos/tests/qtile.nix
+++ b/nixos/tests/qtile.nix
@@ -10,7 +10,7 @@ import ./make-test-python.nix ({ lib, ...} : {
     test-support.displayManager.auto.user = "alice";
 
     services.xserver.windowManager.qtile.enable = true;
-    services.displayManager.defaultSession = lib.mkForce "none+qtile";
+    services.displayManager.defaultSession = lib.mkForce "qtile";
 
     environment.systemPackages = [ pkgs.kitty ];
   };
diff --git a/nixos/tests/vaultwarden.nix b/nixos/tests/vaultwarden.nix
index 28ff170e36107..baefa67dbf535 100644
--- a/nixos/tests/vaultwarden.nix
+++ b/nixos/tests/vaultwarden.nix
@@ -1,38 +1,94 @@
-{ system ? builtins.currentSystem
-, config ? { }
-, pkgs ? import ../.. { inherit system config; }
-}:
-
 # These tests will:
 #  * Set up a vaultwarden server
-#  * Have Firefox use the web vault to create an account, log in, and save a password to the valut
+#  * Have Firefox use the web vault to create an account, log in, and save a password to the vault
 #  * Have the bw cli log in and read that password from the vault
 #
 # Note that Firefox must be on the same machine as the server for WebCrypto APIs to be available (or HTTPS must be configured)
 #
 # The same tests should work without modification on the official bitwarden server, if we ever package that.
 
-with import ../lib/testing-python.nix { inherit system pkgs; };
-with pkgs.lib;
 let
-  backends = [ "sqlite" "mysql" "postgresql" ];
-
-  dbPassword = "please_dont_hack";
-
-  userEmail = "meow@example.com";
-  userPassword = "also_super_secret_ZJWpBKZi668QGt"; # Must be complex to avoid interstitial warning on the signup page
-
-  storedPassword = "seeeecret";
+  makeVaultwardenTest = name: {
+    backend ? name,
+    withClient ? true,
+    testScript ? null,
+  }: import ./make-test-python.nix ({ lib, pkgs, ...}: let
+    dbPassword = "please_dont_hack";
+    userEmail = "meow@example.com";
+    userPassword = "also_super_secret_ZJWpBKZi668QGt"; # Must be complex to avoid interstitial warning on the signup page
+    storedPassword = "seeeecret";
+
+    testRunner = pkgs.writers.writePython3Bin "test-runner" {
+      libraries = [ pkgs.python3Packages.selenium ];
+      flakeIgnore = [  "E501" ];
+    } ''
+
+      from selenium.webdriver.common.by import By
+      from selenium.webdriver import Firefox
+      from selenium.webdriver.firefox.options import Options
+      from selenium.webdriver.support.ui import WebDriverWait
+      from selenium.webdriver.support import expected_conditions as EC
+
+      options = Options()
+      options.add_argument('--headless')
+      driver = Firefox(options=options)
+
+      driver.implicitly_wait(20)
+      driver.get('http://localhost:8080/#/register')
+
+      wait = WebDriverWait(driver, 10)
+
+      wait.until(EC.title_contains("Vaultwarden Web"))
+
+      driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_email').send_keys(
+          '${userEmail}'
+      )
+      driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_name').send_keys(
+          'A Cat'
+      )
+      driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_master-password').send_keys(
+          '${userPassword}'
+      )
+      driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_confirm-master-password').send_keys(
+          '${userPassword}'
+      )
+      if driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').is_selected():
+          driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').click()
+
+      driver.find_element(By.XPATH, "//button[contains(., 'Create account')]").click()
+
+      wait.until_not(EC.title_contains("Create account"))
+
+      driver.find_element(By.XPATH, "//button[contains(., 'Continue')]").click()
+
+      driver.find_element(By.CSS_SELECTOR, 'input#login_input_master-password').send_keys(
+          '${userPassword}'
+      )
+      driver.find_element(By.XPATH, "//button[contains(., 'Log in')]").click()
+
+      wait.until(EC.title_contains("Vaults"))
+
+      driver.find_element(By.XPATH, "//button[contains(., 'New item')]").click()
+
+      driver.find_element(By.CSS_SELECTOR, 'input#name').send_keys(
+          'secrets'
+      )
+      driver.find_element(By.CSS_SELECTOR, 'input#loginPassword').send_keys(
+          '${storedPassword}'
+      )
+
+      driver.find_element(By.XPATH, "//button[contains(., 'Save')]").click()
+    '';
+  in {
+    inherit name;
 
-  makeVaultwardenTest = backend: makeTest {
-    name = "vaultwarden-${backend}";
     meta = {
-      maintainers = with pkgs.lib.maintainers; [ jjjollyjim ];
+      maintainers = with pkgs.lib.maintainers; [ dotlambda SuperSandro2000 ];
     };
 
     nodes = {
-      server = { pkgs, ... }:
-        let backendConfig = {
+      server = { pkgs, ... }: lib.mkMerge [
+        {
           mysql = {
             services.mysql = {
               enable = true;
@@ -53,119 +109,53 @@ let
           postgresql = {
             services.postgresql = {
               enable = true;
-              initialScript = pkgs.writeText "postgresql-init.sql" ''
-                CREATE USER bitwardenuser WITH PASSWORD '${dbPassword}';
-                CREATE DATABASE bitwarden WITH OWNER bitwardenuser;
-              '';
+              ensureDatabases = [ "vaultwarden" ];
+              ensureUsers = [{
+                name = "vaultwarden";
+                ensureDBOwnership = true;
+              }];
             };
 
-            services.vaultwarden.config.databaseUrl = "postgresql://bitwardenuser:${dbPassword}@localhost/bitwarden";
+            services.vaultwarden.config.databaseUrl = "postgresql:///vaultwarden?host=/run/postgresql";
 
             systemd.services.vaultwarden.after = [ "postgresql.service" ];
           };
 
-          sqlite = { };
-        };
-        in
-        mkMerge [
-          backendConfig.${backend}
-          {
-            services.vaultwarden = {
-              enable = true;
-              dbBackend = backend;
-              config = {
-                rocketAddress = "0.0.0.0";
-                rocketPort = 80;
-              };
-            };
+          sqlite = {
+            services.vaultwarden.backupDir = "/var/lib/vaultwarden/backups";
+
+            environment.systemPackages = [ pkgs.sqlite ];
+          };
+        }.${backend}
 
-            networking.firewall.allowedTCPPorts = [ 80 ];
-
-            environment.systemPackages =
-              let
-                testRunner = pkgs.writers.writePython3Bin "test-runner"
-                  {
-                    libraries = [ pkgs.python3Packages.selenium ];
-                    flakeIgnore = [
-                      "E501"
-                    ];
-                  } ''
-
-                  from selenium.webdriver.common.by import By
-                  from selenium.webdriver import Firefox
-                  from selenium.webdriver.firefox.options import Options
-                  from selenium.webdriver.support.ui import WebDriverWait
-                  from selenium.webdriver.support import expected_conditions as EC
-
-                  options = Options()
-                  options.add_argument('--headless')
-                  driver = Firefox(options=options)
-
-                  driver.implicitly_wait(20)
-                  driver.get('http://localhost/#/register')
-
-                  wait = WebDriverWait(driver, 10)
-
-                  wait.until(EC.title_contains("Vaultwarden Web"))
-
-                  driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_email').send_keys(
-                      '${userEmail}'
-                  )
-                  driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_name').send_keys(
-                      'A Cat'
-                  )
-                  driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_master-password').send_keys(
-                      '${userPassword}'
-                  )
-                  driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_confirm-master-password').send_keys(
-                      '${userPassword}'
-                  )
-                  if driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').is_selected():
-                      driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').click()
-
-                  driver.find_element(By.XPATH, "//button[contains(., 'Create account')]").click()
-
-                  wait.until_not(EC.title_contains("Create account"))
-
-                  driver.find_element(By.XPATH, "//button[contains(., 'Continue')]").click()
-
-                  driver.find_element(By.CSS_SELECTOR, 'input#login_input_master-password').send_keys(
-                      '${userPassword}'
-                  )
-                  driver.find_element(By.XPATH, "//button[contains(., 'Log in')]").click()
-
-                  wait.until(EC.title_contains("Vaults"))
-
-                  driver.find_element(By.XPATH, "//button[contains(., 'New item')]").click()
-
-                  driver.find_element(By.CSS_SELECTOR, 'input#name').send_keys(
-                      'secrets'
-                  )
-                  driver.find_element(By.CSS_SELECTOR, 'input#loginPassword').send_keys(
-                      '${storedPassword}'
-                  )
-
-                  driver.find_element(By.XPATH, "//button[contains(., 'Save')]").click()
-                '';
-              in
-              [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
-
-          }
-        ];
-
-      client = { pkgs, ... }:
         {
-          environment.systemPackages = [ pkgs.bitwarden-cli ];
-        };
+          services.vaultwarden = {
+            enable = true;
+            dbBackend = backend;
+            config = {
+              rocketAddress = "0.0.0.0";
+              rocketPort = 8080;
+            };
+          };
+
+          networking.firewall.allowedTCPPorts = [ 8080 ];
+
+          environment.systemPackages = [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
+        }
+      ];
+    } // lib.optionalAttrs withClient {
+      client = { pkgs, ... }: {
+        environment.systemPackages = [ pkgs.bitwarden-cli ];
+      };
     };
 
-    testScript = ''
+    testScript = if testScript != null then testScript else ''
       start_all()
       server.wait_for_unit("vaultwarden.service")
-      server.wait_for_open_port(80)
+      server.wait_for_open_port(8080)
 
       with subtest("configure the cli"):
-          client.succeed("bw --nointeraction config server http://server")
+          client.succeed("bw --nointeraction config server http://server:8080")
 
       with subtest("can't login to nonexistent account"):
           client.fail(
@@ -184,15 +174,40 @@ let
           client.succeed(f"bw --nointeraction --raw --session {key} sync -f")
 
       with subtest("get the password with the cli"):
-          password = client.succeed(
-              f"bw --nointeraction --raw --session {key} list items | ${pkgs.jq}/bin/jq -r .[].login.password"
+          password = client.wait_until_succeeds(
+              f"bw --nointeraction --raw --session {key} list items | ${pkgs.jq}/bin/jq -r .[].login.password",
+              timeout=60
           )
           assert password.strip() == "${storedPassword}"
+
+      with subtest("Check systemd unit hardening"):
+          server.log(server.succeed("systemd-analyze security vaultwarden.service | grep -v ✓"))
     '';
-  };
+  });
 in
-builtins.listToAttrs (
-  map
-    (backend: { name = backend; value = makeVaultwardenTest backend; })
-    backends
-)
+builtins.mapAttrs (k: v: makeVaultwardenTest k v) {
+  mysql = {};
+  postgresql = {};
+  sqlite = {};
+  sqlite-backup = {
+    backend = "sqlite";
+    withClient = false;
+
+    testScript = ''
+      start_all()
+      server.wait_for_unit("vaultwarden.service")
+      server.wait_for_open_port(8080)
+
+      with subtest("Set up vaultwarden"):
+          server.succeed("PYTHONUNBUFFERED=1 test-runner | systemd-cat -t test-runner")
+
+      with subtest("Run the backup script"):
+          server.start_job("backup-vaultwarden.service")
+
+      with subtest("Check that backup exists"):
+          server.succeed('[ -d "/var/lib/vaultwarden/backups" ]')
+          server.succeed('[ -f "/var/lib/vaultwarden/backups/db.sqlite3" ]')
+          server.succeed('[ -d "/var/lib/vaultwarden/backups/attachments" ]')
+    '';
+  };
+}
diff --git a/nixos/tests/ydotool.nix b/nixos/tests/ydotool.nix
index 818ac6f2d50de..45e3d27adeb49 100644
--- a/nixos/tests/ydotool.nix
+++ b/nixos/tests/ydotool.nix
@@ -1,115 +1,184 @@
-import ./make-test-python.nix (
-  { pkgs, lib, ... }:
-  let
-    textInput = "This works.";
-    inputBoxText = "Enter input";
-    inputBox = pkgs.writeShellScript "zenity-input" ''
-      ${lib.getExe pkgs.gnome.zenity} --entry --text '${inputBoxText}:' > /tmp/output &
-    '';
-  in
-  {
-    name = "ydotool";
-
-    meta = {
-      maintainers = with lib.maintainers; [
-        OPNA2608
-        quantenzitrone
-      ];
-    };
+{
+  system ? builtins.currentSystem,
+  config ? { },
+  pkgs ? import ../.. { inherit system config; },
+  lib ? pkgs.lib,
+}:
+let
+  makeTest = import ./make-test-python.nix;
+  textInput = "This works.";
+  inputBoxText = "Enter input";
+  inputBox = pkgs.writeShellScript "zenity-input" ''
+    ${lib.getExe pkgs.gnome.zenity} --entry --text '${inputBoxText}:' > /tmp/output &
+  '';
+  asUser = ''
+    def as_user(cmd: str):
+        """
+        Return a shell command for running a shell command as a specific user.
+        """
+        return f"sudo -u alice -i {cmd}"
+  '';
+in
+{
+  headless = makeTest {
+    name = "headless";
 
-    nodes = {
-      headless =
-        { config, ... }:
-        {
-          imports = [ ./common/user-account.nix ];
+    enableOCR = true;
 
-          users.users.alice.extraGroups = [ "ydotool" ];
+    nodes.machine = {
+      imports = [ ./common/user-account.nix ];
 
-          programs.ydotool.enable = true;
+      users.users.alice.extraGroups = [ "ydotool" ];
 
-          services.getty.autologinUser = "alice";
-        };
+      programs.ydotool.enable = true;
 
-      x11 =
-        { config, ... }:
-        {
-          imports = [
-            ./common/user-account.nix
-            ./common/auto.nix
-            ./common/x11.nix
-          ];
+      services.getty.autologinUser = "alice";
+    };
 
-          users.users.alice.extraGroups = [ "ydotool" ];
+    testScript =
+      asUser
+      + ''
+        start_all()
 
-          programs.ydotool.enable = true;
+        machine.wait_for_unit("multi-user.target")
+        machine.wait_for_text("alice")
+        machine.succeed(as_user("ydotool type 'echo ${textInput} > /tmp/output'")) # text input
+        machine.succeed(as_user("ydotool key 28:1 28:0")) # text input
+        machine.screenshot("headless_input")
+        machine.wait_for_file("/tmp/output")
+        machine.wait_until_succeeds("grep '${textInput}' /tmp/output") # text input
+      '';
 
-          test-support.displayManager.auto = {
-            enable = true;
-            user = "alice";
-          };
+    meta.maintainers = with lib.maintainers; [
+      OPNA2608
+      quantenzitrone
+    ];
+  };
 
-          services.xserver.windowManager.dwm.enable = true;
-          services.displayManager.defaultSession = lib.mkForce "none+dwm";
-        };
+  x11 = makeTest {
+    name = "x11";
 
-      wayland =
-        { config, ... }:
-        {
-          imports = [ ./common/user-account.nix ];
+    enableOCR = true;
 
-          services.cage = {
-            enable = true;
-            user = "alice";
-          };
+    nodes.machine = {
+      imports = [
+        ./common/user-account.nix
+        ./common/auto.nix
+        ./common/x11.nix
+      ];
 
-          programs.ydotool.enable = true;
+      users.users.alice.extraGroups = [ "ydotool" ];
 
-          services.cage.program = inputBox;
-        };
+      programs.ydotool.enable = true;
+
+      test-support.displayManager.auto = {
+        enable = true;
+        user = "alice";
+      };
+
+      services.xserver.windowManager.dwm.enable = true;
+      services.displayManager.defaultSession = lib.mkForce "none+dwm";
     };
 
+    testScript =
+      asUser
+      + ''
+        start_all()
+
+        machine.wait_for_x()
+        machine.execute(as_user("${inputBox}"))
+        machine.wait_for_text("${inputBoxText}")
+        machine.succeed(as_user("ydotool type '${textInput}'")) # text input
+        machine.screenshot("x11_input")
+        machine.succeed(as_user("ydotool mousemove -a 400 110")) # mouse input
+        machine.succeed(as_user("ydotool click 0xC0")) # mouse input
+        machine.wait_for_file("/tmp/output")
+        machine.wait_until_succeeds("grep '${textInput}' /tmp/output") # text input
+      '';
+
+    meta.maintainers = with lib.maintainers; [
+      OPNA2608
+      quantenzitrone
+    ];
+  };
+
+  wayland = makeTest {
+    name = "wayland";
+
     enableOCR = true;
 
-    testScript =
-      { nodes, ... }:
-      ''
-        def as_user(cmd: str):
-          """
-          Return a shell command for running a shell command as a specific user.
-          """
-          return f"sudo -u alice -i {cmd}"
+    nodes.machine = {
+      imports = [ ./common/user-account.nix ];
+
+      services.cage = {
+        enable = true;
+        user = "alice";
+      };
+
+      programs.ydotool.enable = true;
+
+      services.cage.program = inputBox;
+    };
+
+    testScript = ''
+      start_all()
+
+      machine.wait_for_unit("graphical.target")
+      machine.wait_for_text("${inputBoxText}")
+      machine.succeed("ydotool type '${textInput}'") # text input
+      machine.screenshot("wayland_input")
+      machine.succeed("ydotool mousemove -a 100 100") # mouse input
+      machine.succeed("ydotool click 0xC0") # mouse input
+      machine.wait_for_file("/tmp/output")
+      machine.wait_until_succeeds("grep '${textInput}' /tmp/output") # text input
+    '';
+
+    meta.maintainers = with lib.maintainers; [
+      OPNA2608
+      quantenzitrone
+    ];
+  };
+
+  customGroup =
+    let
+      name = "customGroup";
+      nodeName = "${name}Node";
+      insideGroupUsername = "ydotool-user";
+      outsideGroupUsername = "other-user";
+      groupName = "custom-group";
+    in
+    makeTest {
+      inherit name;
+
+      nodes."${nodeName}" = {
+        programs.ydotool = {
+          enable = true;
+          group = groupName;
+        };
+
+        users.users = {
+          "${insideGroupUsername}" = {
+            isNormalUser = true;
+            extraGroups = [ groupName ];
+          };
+          "${outsideGroupUsername}".isNormalUser = true;
+        };
+      };
 
+      testScript = ''
         start_all()
 
-        # Headless
-        headless.wait_for_unit("multi-user.target")
-        headless.wait_for_text("alice")
-        headless.succeed(as_user("ydotool type 'echo ${textInput} > /tmp/output'")) # text input
-        headless.succeed(as_user("ydotool key 28:1 28:0")) # text input
-        headless.screenshot("headless_input")
-        headless.wait_for_file("/tmp/output")
-        headless.wait_until_succeeds("grep '${textInput}' /tmp/output") # text input
-
-        # X11
-        x11.wait_for_x()
-        x11.execute(as_user("${inputBox}"))
-        x11.wait_for_text("${inputBoxText}")
-        x11.succeed(as_user("ydotool type '${textInput}'")) # text input
-        x11.screenshot("x11_input")
-        x11.succeed(as_user("ydotool mousemove -a 400 110")) # mouse input
-        x11.succeed(as_user("ydotool click 0xC0")) # mouse input
-        x11.wait_for_file("/tmp/output")
-        x11.wait_until_succeeds("grep '${textInput}' /tmp/output") # text input
-
-        # Wayland
-        wayland.wait_for_unit("graphical.target")
-        wayland.wait_for_text("${inputBoxText}")
-        wayland.succeed("ydotool type '${textInput}'") # text input
-        wayland.screenshot("wayland_input")
-        wayland.succeed("ydotool mousemove -a 100 100") # mouse input
-        wayland.succeed("ydotool click 0xC0") # mouse input
-        wayland.wait_for_file("/tmp/output")
-        wayland.wait_until_succeeds("grep '${textInput}' /tmp/output") # text input
+        # Wait for service to start
+        ${nodeName}.wait_for_unit("multi-user.target")
+        ${nodeName}.wait_for_unit("ydotoold.service")
+
+        # Verify that user with the configured group can use the service
+        ${nodeName}.succeed("sudo --login --user=${insideGroupUsername} ydotool type 'Hello, World!'")
+
+        # Verify that user without the configured group can't use the service
+        ${nodeName}.fail("sudo --login --user=${outsideGroupUsername} ydotool type 'Hello, World!'")
       '';
-  }
-)
+
+      meta.maintainers = with lib.maintainers; [ l0b0 ];
+    };
+}