1 { config, lib, pkgs, ... }:
4 cfg = config.services.vaultwarden;
5 user = config.users.users.vaultwarden.name;
6 group = config.users.groups.vaultwarden.name;
8 StateDirectory = if lib.versionOlder config.system.stateVersion "24.11" then "bitwarden_rs" else "vaultwarden";
10 dataDir = "/var/lib/${StateDirectory}";
12 # Convert name from camel case (e.g. disable2FARemember) to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
15 parts = builtins.split "([A-Z0-9]+)" name;
16 partsToEnvVar = parts: lib.foldl' (key: x: let last = lib.stringLength key - 1; in
17 if lib.isList x then key + lib.optionalString (key != "" && lib.substring last 1 key != "_") "_" + lib.head x
18 else if key != "" && lib.elem (lib.substring 0 1 x) lib.lowerChars then # to handle e.g. [ "disable" [ "2FAR" ] "emember" ]
19 lib.substring 0 last key + lib.optionalString (lib.substring (last - 1) 1 key != "_") "_" + lib.substring last 1 key + lib.toUpper x
20 else key + lib.toUpper x) "" parts;
21 in if builtins.match "[A-Z0-9_]+" name != null then name else partsToEnvVar parts;
23 # Due to the different naming schemes allowed for config keys,
24 # we can only check for values consistently after converting them to their corresponding environment variable name.
27 configEnv = lib.concatMapAttrs (name: value: lib.optionalAttrs (value != null) {
28 ${nameToEnvVar name} = if lib.isBool value then lib.boolToString value else toString value;
30 in { DATA_FOLDER = dataDir; } // lib.optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
31 WEB_VAULT_FOLDER = "${cfg.webVaultPackage}/share/vaultwarden/vault";
34 configFile = pkgs.writeText "vaultwarden.env" (lib.concatStrings (lib.mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
36 vaultwarden = cfg.package.override { inherit (cfg) dbBackend; };
40 (lib.mkRenamedOptionModule [ "services" "bitwarden_rs" ] [ "services" "vaultwarden" ])
43 options.services.vaultwarden = {
44 enable = lib.mkEnableOption "vaultwarden";
46 dbBackend = lib.mkOption {
47 type = lib.types.enum [ "sqlite" "mysql" "postgresql" ];
50 Which database backend vaultwarden will be using.
54 backupDir = lib.mkOption {
55 type = with lib.types; nullOr str;
58 The directory under which vaultwarden will backup its persistent data.
60 example = "/var/backup/vaultwarden";
63 config = lib.mkOption {
64 type = with lib.types; attrsOf (nullOr (oneOf [ bool int str ]));
66 ROCKET_ADDRESS = "::1"; # default to localhost
69 example = lib.literalExpression ''
71 DOMAIN = "https://bitwarden.example.com";
72 SIGNUPS_ALLOWED = false;
74 # Vaultwarden currently recommends running behind a reverse proxy
75 # (nginx or similar) for TLS termination, see
76 # https://github.com/dani-garcia/vaultwarden/wiki/Hardening-Guide#reverse-proxying
77 # > you should avoid enabling HTTPS via vaultwarden's built-in Rocket TLS support,
78 # > especially if your instance is publicly accessible.
80 # A suitable NixOS nginx reverse proxy example config might be:
82 # services.nginx.virtualHosts."bitwarden.example.com" = {
86 # proxyPass = "http://127.0.0.1:''${toString config.services.vaultwarden.config.ROCKET_PORT}";
89 ROCKET_ADDRESS = "127.0.0.1";
92 ROCKET_LOG = "critical";
94 # This example assumes a mailserver running on localhost,
95 # thus without transport encryption.
96 # If you use an external mail server, follow:
97 # https://github.com/dani-garcia/vaultwarden/wiki/SMTP-configuration
98 SMTP_HOST = "127.0.0.1";
102 SMTP_FROM = "admin@bitwarden.example.com";
103 SMTP_FROM_NAME = "example.com Bitwarden server";
107 The configuration of vaultwarden is done through environment variables,
108 therefore it is recommended to use upper snake case (e.g. {env}`DISABLE_2FA_REMEMBER`).
110 However, camel case (e.g. `disable2FARemember`) is also supported:
111 The NixOS module will convert it automatically to
112 upper case snake case (e.g. {env}`DISABLE_2FA_REMEMBER`).
113 In this conversion digits (0-9) are handled just like upper case characters,
114 so `foo2` would be converted to {env}`FOO_2`.
115 Names already in this format remain unchanged, so `FOO2` remains `FOO2` if passed as such,
116 even though `foo2` would have been converted to {env}`FOO_2`.
117 This allows working around any potential future conflicting naming conventions.
119 Based on the attributes passed to this config option an environment file will be generated
120 that is passed to vaultwarden's systemd service.
122 The available configuration options can be found in
123 [the environment template file](https://github.com/dani-garcia/vaultwarden/blob/${vaultwarden.version}/.env.template).
125 See [](#opt-services.vaultwarden.environmentFile) for how
126 to set up access to the Admin UI to invite initial users.
130 environmentFile = lib.mkOption {
131 type = with lib.types; nullOr path;
133 example = "/var/lib/vaultwarden.env";
135 Additional environment file as defined in {manpage}`systemd.exec(5)`.
137 Secrets like {env}`ADMIN_TOKEN` and {env}`SMTP_PASSWORD`
138 should be passed to the service without adding them to the world-readable Nix store.
140 Note that this file needs to be available on the host on which `vaultwarden` is running.
142 As a concrete example, to make the Admin UI available (from which new users can be invited initially),
143 the secret {env}`ADMIN_TOKEN` needs to be defined as described
144 [here](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page):
147 # Admin secret token, see
148 # https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page
149 ADMIN_TOKEN=...copy-paste a unique generated secret token here...
154 package = lib.mkPackageOption pkgs "vaultwarden" { };
156 webVaultPackage = lib.mkOption {
157 type = lib.types.package;
158 default = pkgs.vaultwarden.webvault;
159 defaultText = lib.literalExpression "pkgs.vaultwarden.webvault";
160 description = "Web vault package to use.";
164 config = lib.mkIf cfg.enable {
167 assertion = cfg.backupDir != null -> cfg.dbBackend == "sqlite";
168 message = "Backups for database backends other than sqlite will need customization";
171 assertion = cfg.backupDir != null -> !(lib.hasPrefix dataDir cfg.backupDir);
172 message = "Backup directory can not be in ${dataDir}";
176 users.users.vaultwarden = {
180 users.groups.vaultwarden = { };
182 systemd.services.vaultwarden = {
183 after = [ "network.target" ];
184 path = with pkgs; [ openssl ];
188 EnvironmentFile = [ configFile ] ++ lib.optional (cfg.environmentFile != null) cfg.environmentFile;
189 ExecStart = lib.getExe vaultwarden;
190 LimitNOFILE = "1048576";
191 CapabilityBoundingSet = [ "" ];
192 DeviceAllow = [ "" ];
193 DevicePolicy = "closed";
194 LockPersonality = true;
195 MemoryDenyWriteExecute = true;
196 NoNewPrivileges = true;
197 PrivateDevices = true;
202 ProtectControlGroups = true;
204 ProtectHostname = true;
205 ProtectKernelLogs = true;
206 ProtectKernelModules = true;
207 ProtectKernelTunables = true;
208 ProtectProc = "noaccess";
209 ProtectSystem = "strict";
211 RestrictAddressFamilies = [
216 RestrictNamespaces = true;
217 RestrictRealtime = true;
218 RestrictSUIDSGID = true;
219 inherit StateDirectory;
220 StateDirectoryMode = "0700";
221 SystemCallArchitectures = "native";
229 wantedBy = [ "multi-user.target" ];
232 systemd.services.backup-vaultwarden = lib.mkIf (cfg.backupDir != null) {
233 description = "Backup vaultwarden";
235 DATA_FOLDER = dataDir;
236 BACKUP_FOLDER = cfg.backupDir;
238 path = with pkgs; [ sqlite ];
239 # if both services are started at the same time, vaultwarden fails with "database is locked"
240 before = [ "vaultwarden.service" ];
242 SyslogIdentifier = "backup-vaultwarden";
244 User = lib.mkDefault user;
245 Group = lib.mkDefault group;
246 ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}";
248 wantedBy = [ "multi-user.target" ];
251 systemd.timers.backup-vaultwarden = lib.mkIf (cfg.backupDir != null) {
252 description = "Backup vaultwarden on time";
254 OnCalendar = lib.mkDefault "23:00";
256 Unit = "backup-vaultwarden.service";
258 wantedBy = [ "multi-user.target" ];
261 systemd.tmpfiles.settings = lib.mkIf (cfg.backupDir != null) {
262 "10-vaultwarden".${cfg.backupDir}.d = {
270 # uses attributes of the linked package
271 buildDocsInSandbox = false;
272 maintainers = with lib.maintainers; [ dotlambda SuperSandro2000 ];