1 { config, lib, pkgs, ... }:
6 cfg = config.services.vaultwarden;
7 user = config.users.users.vaultwarden.name;
8 group = config.users.groups.vaultwarden.name;
10 # Convert name from camel case (e.g. disable2FARemember) to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
13 parts = builtins.split "([A-Z0-9]+)" name;
14 partsToEnvVar = parts: foldl' (key: x: let last = stringLength key - 1; in
15 if isList x then key + optionalString (key != "" && substring last 1 key != "_") "_" + head x
16 else if key != "" && elem (substring 0 1 x) lowerChars then # to handle e.g. [ "disable" [ "2FAR" ] "emember" ]
17 substring 0 last key + optionalString (substring (last - 1) 1 key != "_") "_" + substring last 1 key + toUpper x
18 else key + toUpper x) "" parts;
19 in if builtins.match "[A-Z0-9_]+" name != null then name else partsToEnvVar parts;
21 # Due to the different naming schemes allowed for config keys,
22 # we can only check for values consistently after converting them to their corresponding environment variable name.
25 configEnv = listToAttrs (concatLists (mapAttrsToList (name: value:
26 if value != null then [ (nameValuePair (nameToEnvVar name) (if isBool value then boolToString value else toString value)) ] else []
28 in { DATA_FOLDER = "/var/lib/bitwarden_rs"; } // optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
29 WEB_VAULT_FOLDER = "${cfg.webVaultPackage}/share/vaultwarden/vault";
32 configFile = pkgs.writeText "vaultwarden.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
34 vaultwarden = cfg.package.override { inherit (cfg) dbBackend; };
38 (mkRenamedOptionModule [ "services" "bitwarden_rs" ] [ "services" "vaultwarden" ])
41 options.services.vaultwarden = with types; {
42 enable = mkEnableOption (lib.mdDoc "vaultwarden");
44 dbBackend = mkOption {
45 type = enum [ "sqlite" "mysql" "postgresql" ];
47 description = lib.mdDoc ''
48 Which database backend vaultwarden will be using.
52 backupDir = mkOption {
55 description = lib.mdDoc ''
56 The directory under which vaultwarden will backup its persistent data.
61 type = attrsOf (nullOr (oneOf [ bool int str ]));
63 example = literalExpression ''
65 DOMAIN = "https://bitwarden.example.com";
66 SIGNUPS_ALLOWED = false;
68 # Vaultwarden currently recommends running behind a reverse proxy
69 # (nginx or similar) for TLS termination, see
70 # https://github.com/dani-garcia/vaultwarden/wiki/Hardening-Guide#reverse-proxying
71 # > you should avoid enabling HTTPS via vaultwarden's built-in Rocket TLS support,
72 # > especially if your instance is publicly accessible.
74 # A suitable NixOS nginx reverse proxy example config might be:
76 # services.nginx.virtualHosts."bitwarden.example.com" = {
80 # proxyPass = "http://127.0.0.1:''${toString config.services.vaultwarden.config.ROCKET_PORT}";
83 ROCKET_ADDRESS = "127.0.0.1";
86 ROCKET_LOG = "critical";
88 # This example assumes a mailserver running on localhost,
89 # thus without transport encryption.
90 # If you use an external mail server, follow:
91 # https://github.com/dani-garcia/vaultwarden/wiki/SMTP-configuration
92 SMTP_HOST = "127.0.0.1";
96 SMTP_FROM = "admin@bitwarden.example.com";
97 SMTP_FROM_NAME = "example.com Bitwarden server";
100 description = lib.mdDoc ''
101 The configuration of vaultwarden is done through environment variables,
102 therefore it is recommended to use upper snake case (e.g. {env}`DISABLE_2FA_REMEMBER`).
104 However, camel case (e.g. `disable2FARemember`) is also supported:
105 The NixOS module will convert it automatically to
106 upper case snake case (e.g. {env}`DISABLE_2FA_REMEMBER`).
107 In this conversion digits (0-9) are handled just like upper case characters,
108 so `foo2` would be converted to {env}`FOO_2`.
109 Names already in this format remain unchanged, so `FOO2` remains `FOO2` if passed as such,
110 even though `foo2` would have been converted to {env}`FOO_2`.
111 This allows working around any potential future conflicting naming conventions.
113 Based on the attributes passed to this config option an environment file will be generated
114 that is passed to vaultwarden's systemd service.
116 The available configuration options can be found in
117 [the environment template file](https://github.com/dani-garcia/vaultwarden/blob/${vaultwarden.version}/.env.template).
119 See ()[#opt-services.vaultwarden.environmentFile) for how
120 to set up access to the Admin UI to invite initial users.
124 environmentFile = mkOption {
125 type = with types; nullOr path;
127 example = "/var/lib/vaultwarden.env";
128 description = lib.mdDoc ''
129 Additional environment file as defined in {manpage}`systemd.exec(5)`.
131 Secrets like {env}`ADMIN_TOKEN` and {env}`SMTP_PASSWORD`
132 may be passed to the service without adding them to the world-readable Nix store.
134 Note that this file needs to be available on the host on which
135 `vaultwarden` is running.
137 As a concrete example, to make the Admin UI available
138 (from which new users can be invited initially),
139 the secret {env}`ADMIN_TOKEN` needs to be defined as described
140 [here](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page).
141 Setting `environmentFile` to `/var/lib/vaultwarden.env`
142 and ensuring permissions with e.g.
143 `chown vaultwarden:vaultwarden /var/lib/vaultwarden.env`
144 (the `vaultwarden` user will only exist after activating with
145 `enable = true;` before this), we can set the contents of the file to have
149 # Admin secret token, see
150 # https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page
151 ADMIN_TOKEN=...copy-paste a unique generated secret token here...
158 default = pkgs.vaultwarden;
159 defaultText = literalExpression "pkgs.vaultwarden";
160 description = lib.mdDoc "Vaultwarden package to use.";
163 webVaultPackage = mkOption {
165 default = pkgs.vaultwarden-vault;
166 defaultText = literalExpression "pkgs.vaultwarden-vault";
167 description = lib.mdDoc "Web vault package to use.";
171 config = mkIf cfg.enable {
173 assertion = cfg.backupDir != null -> cfg.dbBackend == "sqlite";
174 message = "Backups for database backends other than sqlite will need customization";
177 users.users.vaultwarden = {
181 users.groups.vaultwarden = { };
183 systemd.services.vaultwarden = {
184 aliases = [ "bitwarden_rs.service" ];
185 after = [ "network.target" ];
186 path = with pkgs; [ openssl ];
190 EnvironmentFile = [ configFile ] ++ optional (cfg.environmentFile != null) cfg.environmentFile;
191 ExecStart = "${vaultwarden}/bin/vaultwarden";
192 LimitNOFILE = "1048576";
194 PrivateDevices = "true";
195 ProtectHome = "true";
196 ProtectSystem = "strict";
197 AmbientCapabilities = "CAP_NET_BIND_SERVICE";
198 StateDirectory = "bitwarden_rs";
199 StateDirectoryMode = "0700";
202 wantedBy = [ "multi-user.target" ];
205 systemd.services.backup-vaultwarden = mkIf (cfg.backupDir != null) {
206 aliases = [ "backup-bitwarden_rs.service" ];
207 description = "Backup vaultwarden";
209 DATA_FOLDER = "/var/lib/bitwarden_rs";
210 BACKUP_FOLDER = cfg.backupDir;
212 path = with pkgs; [ sqlite ];
213 # if both services are started at the same time, vaultwarden fails with "database is locked"
214 before = [ "vaultwarden.service" ];
216 SyslogIdentifier = "backup-vaultwarden";
218 User = mkDefault user;
219 Group = mkDefault group;
220 ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}";
222 wantedBy = [ "multi-user.target" ];
225 systemd.timers.backup-vaultwarden = mkIf (cfg.backupDir != null) {
226 aliases = [ "backup-bitwarden_rs.timer" ];
227 description = "Backup vaultwarden on time";
229 OnCalendar = mkDefault "23:00";
231 Unit = "backup-vaultwarden.service";
233 wantedBy = [ "multi-user.target" ];
237 # uses attributes of the linked package
238 meta.buildDocsInSandbox = false;