2 # * Set up a vaultwarden server
3 # * Have Firefox use the web vault to create an account, log in, and save a password to the vault
4 # * Have the bw cli log in and read that password from the vault
6 # Note that Firefox must be on the same machine as the server for WebCrypto APIs to be available (or HTTPS must be configured)
8 # The same tests should work without modification on the official bitwarden server, if we ever package that.
11 makeVaultwardenTest = name: {
15 }: import ./make-test-python.nix ({ lib, pkgs, ...}: let
16 dbPassword = "please_dont_hack";
17 userEmail = "meow@example.com";
18 userPassword = "also_super_secret_ZJWpBKZi668QGt"; # Must be complex to avoid interstitial warning on the signup page
19 storedPassword = "seeeecret";
21 testRunner = pkgs.writers.writePython3Bin "test-runner" {
22 libraries = [ pkgs.python3Packages.selenium ];
23 flakeIgnore = [ "E501" ];
26 from selenium.webdriver.common.by import By
27 from selenium.webdriver import Firefox
28 from selenium.webdriver.firefox.options import Options
29 from selenium.webdriver.support.ui import WebDriverWait
30 from selenium.webdriver.support import expected_conditions as EC
33 options.add_argument('--headless')
34 driver = Firefox(options=options)
36 driver.implicitly_wait(20)
37 driver.get('http://localhost:8080/#/register')
39 wait = WebDriverWait(driver, 10)
41 wait.until(EC.title_contains("Vaultwarden Web"))
43 driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_email').send_keys(
46 driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_name').send_keys(
49 driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_master-password').send_keys(
52 driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_confirm-master-password').send_keys(
55 if driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').is_selected():
56 driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').click()
58 driver.find_element(By.XPATH, "//button[contains(., 'Create account')]").click()
60 wait.until_not(EC.title_contains("Create account"))
62 driver.find_element(By.XPATH, "//button[contains(., 'Continue')]").click()
64 driver.find_element(By.CSS_SELECTOR, 'input#login_input_master-password').send_keys(
67 driver.find_element(By.XPATH, "//button[contains(., 'Log in')]").click()
69 wait.until(EC.title_contains("Vaults"))
71 driver.find_element(By.XPATH, "//button[contains(., 'New item')]").click()
73 driver.find_element(By.CSS_SELECTOR, 'input#name').send_keys(
76 driver.find_element(By.CSS_SELECTOR, 'input#loginPassword').send_keys(
80 driver.find_element(By.XPATH, "//button[contains(., 'Save')]").click()
86 maintainers = with pkgs.lib.maintainers; [ dotlambda SuperSandro2000 ];
90 server = { pkgs, ... }: lib.mkMerge [
95 initialScript = pkgs.writeText "mysql-init.sql" ''
96 CREATE DATABASE bitwarden;
97 CREATE USER 'bitwardenuser'@'localhost' IDENTIFIED BY '${dbPassword}';
98 GRANT ALL ON `bitwarden`.* TO 'bitwardenuser'@'localhost';
101 package = pkgs.mariadb;
104 services.vaultwarden.config.databaseUrl = "mysql://bitwardenuser:${dbPassword}@localhost/bitwarden";
106 systemd.services.vaultwarden.after = [ "mysql.service" ];
110 services.postgresql = {
112 ensureDatabases = [ "vaultwarden" ];
114 name = "vaultwarden";
115 ensureDBOwnership = true;
119 services.vaultwarden.config.databaseUrl = "postgresql:///vaultwarden?host=/run/postgresql";
121 systemd.services.vaultwarden.after = [ "postgresql.service" ];
125 services.vaultwarden.backupDir = "/srv/backups/vaultwarden";
127 environment.systemPackages = [ pkgs.sqlite ];
132 services.vaultwarden = {
136 rocketAddress = "::";
141 networking.firewall.allowedTCPPorts = [ 8080 ];
143 environment.systemPackages = [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
146 } // lib.optionalAttrs withClient {
147 client = { pkgs, ... }: {
148 environment.systemPackages = [ pkgs.bitwarden-cli ];
152 testScript = if testScript != null then testScript else ''
154 server.wait_for_unit("vaultwarden.service")
155 server.wait_for_open_port(8080)
157 with subtest("configure the cli"):
158 client.succeed("bw --nointeraction config server http://server:8080")
160 with subtest("can't login to nonexistent account"):
162 "bw --nointeraction --raw login ${userEmail} ${userPassword}"
165 with subtest("use the web interface to sign up, log in, and save a password"):
166 server.succeed("PYTHONUNBUFFERED=1 systemd-cat -t test-runner test-runner")
168 with subtest("log in with the cli"):
169 key = client.succeed(
170 "bw --nointeraction --raw login ${userEmail} ${userPassword}"
173 with subtest("sync with the cli"):
174 client.succeed(f"bw --nointeraction --raw --session {key} sync -f")
176 with subtest("get the password with the cli"):
177 password = client.wait_until_succeeds(
178 f"bw --nointeraction --raw --session {key} list items | ${pkgs.jq}/bin/jq -r .[].login.password",
181 assert password.strip() == "${storedPassword}"
183 with subtest("Check systemd unit hardening"):
184 server.log(server.succeed("systemd-analyze security vaultwarden.service | grep -v ✓"))
188 builtins.mapAttrs (k: v: makeVaultwardenTest k v) {
198 server.wait_for_unit("vaultwarden.service")
199 server.wait_for_open_port(8080)
201 with subtest("Set up vaultwarden"):
202 server.succeed("PYTHONUNBUFFERED=1 test-runner | systemd-cat -t test-runner")
204 with subtest("Run the backup script"):
205 server.start_job("backup-vaultwarden.service")
207 with subtest("Check that backup exists"):
208 server.succeed('[ -d "/srv/backups/vaultwarden" ]')
209 server.succeed('[ -f "/srv/backups/vaultwarden/db.sqlite3" ]')
210 server.succeed('[ -d "/srv/backups/vaultwarden/attachments" ]')
211 server.succeed('[ -f "/srv/backups/vaultwarden/rsa_key.pem" ]')
212 # Ensure only the db backed up with the backup command exists and not the other db files.
213 server.succeed('[ ! -f "/srv/backups/vaultwarden/db.sqlite3-shm" ]')