1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.vaultwarden;
10 user = config.users.users.vaultwarden.name;
11 group = config.users.groups.vaultwarden.name;
12
13 StateDirectory =
14 if lib.versionOlder config.system.stateVersion "24.11" then "bitwarden_rs" else "vaultwarden";
15
16 dataDir = "/var/lib/${StateDirectory}";
17
18 # Convert name from camel case (e.g. disable2FARemember) to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
19 nameToEnvVar =
20 name:
21 let
22 parts = builtins.split "([A-Z0-9]+)" name;
23 partsToEnvVar =
24 parts:
25 lib.foldl' (
26 key: x:
27 let
28 last = lib.stringLength key - 1;
29 in
30 if lib.isList x then
31 key + lib.optionalString (key != "" && lib.substring last 1 key != "_") "_" + lib.head x
32 else if key != "" && lib.elem (lib.substring 0 1 x) lib.lowerChars then # to handle e.g. [ "disable" [ "2FAR" ] "emember" ]
33 lib.substring 0 last key
34 + lib.optionalString (lib.substring (last - 1) 1 key != "_") "_"
35 + lib.substring last 1 key
36 + lib.toUpper x
37 else
38 key + lib.toUpper x
39 ) "" parts;
40 in
41 if builtins.match "[A-Z0-9_]+" name != null then name else partsToEnvVar parts;
42
43 # Due to the different naming schemes allowed for config keys,
44 # we can only check for values consistently after converting them to their corresponding environment variable name.
45 configEnv =
46 let
47 configEnv = lib.concatMapAttrs (
48 name: value:
49 lib.optionalAttrs (value != null) {
50 ${nameToEnvVar name} = if lib.isBool value then lib.boolToString value else toString value;
51 }
52 ) cfg.config;
53 in
54 {
55 DATA_FOLDER = dataDir;
56 }
57 // lib.optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
58 WEB_VAULT_FOLDER = "${cfg.webVaultPackage}/share/vaultwarden/vault";
59 }
60 // configEnv;
61
62 configFile = pkgs.writeText "vaultwarden.env" (
63 lib.concatStrings (lib.mapAttrsToList (name: value: "${name}=${value}\n") configEnv)
64 );
65
66 vaultwarden = cfg.package.override { inherit (cfg) dbBackend; };
67
68 useSendmail = configEnv.USE_SENDMAIL or null == "true";
69in
70{
71 imports = [
72 (lib.mkRenamedOptionModule [ "services" "bitwarden_rs" ] [ "services" "vaultwarden" ])
73 ];
74
75 options.services.vaultwarden = {
76 enable = lib.mkEnableOption "vaultwarden";
77
78 dbBackend = lib.mkOption {
79 type = lib.types.enum [
80 "sqlite"
81 "mysql"
82 "postgresql"
83 ];
84 default = "sqlite";
85 description = ''
86 Which database backend vaultwarden will be using.
87 '';
88 };
89
90 backupDir = lib.mkOption {
91 type = with lib.types; nullOr str;
92 default = null;
93 description = ''
94 The directory under which vaultwarden will backup its persistent data.
95 '';
96 example = "/var/backup/vaultwarden";
97 };
98
99 config = lib.mkOption {
100 type =
101 with lib.types;
102 attrsOf (
103 nullOr (oneOf [
104 bool
105 int
106 str
107 ])
108 );
109 default = {
110 ROCKET_ADDRESS = "::1"; # default to localhost
111 ROCKET_PORT = 8222;
112 };
113 example = lib.literalExpression ''
114 {
115 DOMAIN = "https://bitwarden.example.com";
116 SIGNUPS_ALLOWED = false;
117
118 # Vaultwarden currently recommends running behind a reverse proxy
119 # (nginx or similar) for TLS termination, see
120 # https://github.com/dani-garcia/vaultwarden/wiki/Hardening-Guide#reverse-proxying
121 # > you should avoid enabling HTTPS via vaultwarden's built-in Rocket TLS support,
122 # > especially if your instance is publicly accessible.
123 #
124 # A suitable NixOS nginx reverse proxy example config might be:
125 #
126 # services.nginx.virtualHosts."bitwarden.example.com" = {
127 # enableACME = true;
128 # forceSSL = true;
129 # locations."/" = {
130 # proxyPass = "http://127.0.0.1:''${toString config.services.vaultwarden.config.ROCKET_PORT}";
131 # };
132 # };
133 ROCKET_ADDRESS = "127.0.0.1";
134 ROCKET_PORT = 8222;
135
136 ROCKET_LOG = "critical";
137
138 # This example assumes a mailserver running on localhost,
139 # thus without transport encryption.
140 # If you use an external mail server, follow:
141 # https://github.com/dani-garcia/vaultwarden/wiki/SMTP-configuration
142 SMTP_HOST = "127.0.0.1";
143 SMTP_PORT = 25;
144 SMTP_SSL = false;
145
146 SMTP_FROM = "admin@bitwarden.example.com";
147 SMTP_FROM_NAME = "example.com Bitwarden server";
148 }
149 '';
150 description = ''
151 The configuration of vaultwarden is done through environment variables,
152 therefore it is recommended to use upper snake case (e.g. {env}`DISABLE_2FA_REMEMBER`).
153
154 However, camel case (e.g. `disable2FARemember`) is also supported:
155 The NixOS module will convert it automatically to
156 upper case snake case (e.g. {env}`DISABLE_2FA_REMEMBER`).
157 In this conversion digits (0-9) are handled just like upper case characters,
158 so `foo2` would be converted to {env}`FOO_2`.
159 Names already in this format remain unchanged, so `FOO2` remains `FOO2` if passed as such,
160 even though `foo2` would have been converted to {env}`FOO_2`.
161 This allows working around any potential future conflicting naming conventions.
162
163 Based on the attributes passed to this config option an environment file will be generated
164 that is passed to vaultwarden's systemd service.
165
166 The available configuration options can be found in
167 [the environment template file](https://github.com/dani-garcia/vaultwarden/blob/${vaultwarden.version}/.env.template).
168
169 See [](#opt-services.vaultwarden.environmentFile) for how
170 to set up access to the Admin UI to invite initial users.
171 '';
172 };
173
174 environmentFile = lib.mkOption {
175 type = with lib.types; nullOr path;
176 default = null;
177 example = "/var/lib/vaultwarden.env";
178 description = ''
179 Additional environment file as defined in {manpage}`systemd.exec(5)`.
180
181 Secrets like {env}`ADMIN_TOKEN` and {env}`SMTP_PASSWORD`
182 should be passed to the service without adding them to the world-readable Nix store.
183
184 Note that this file needs to be available on the host on which `vaultwarden` is running.
185
186 As a concrete example, to make the Admin UI available (from which new users can be invited initially),
187 the secret {env}`ADMIN_TOKEN` needs to be defined as described
188 [here](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page):
189
190 ```
191 # Admin secret token, see
192 # https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page
193 ADMIN_TOKEN=...copy-paste a unique generated secret token here...
194 ```
195 '';
196 };
197
198 package = lib.mkPackageOption pkgs "vaultwarden" { };
199
200 webVaultPackage = lib.mkOption {
201 type = lib.types.package;
202 default = pkgs.vaultwarden.webvault;
203 defaultText = lib.literalExpression "pkgs.vaultwarden.webvault";
204 description = "Web vault package to use.";
205 };
206 };
207
208 config = lib.mkIf cfg.enable {
209 assertions = [
210 {
211 assertion = cfg.backupDir != null -> cfg.dbBackend == "sqlite";
212 message = "Backups for database backends other than sqlite will need customization";
213 }
214 {
215 assertion = cfg.backupDir != null -> !(lib.hasPrefix dataDir cfg.backupDir);
216 message = "Backup directory can not be in ${dataDir}";
217 }
218 ];
219
220 users.users.vaultwarden = {
221 inherit group;
222 isSystemUser = true;
223 };
224 users.groups.vaultwarden = { };
225
226 systemd.services.vaultwarden = {
227 after = [ "network.target" ];
228 path = with pkgs; [ openssl ];
229 serviceConfig = {
230 User = user;
231 Group = group;
232 EnvironmentFile = [ configFile ] ++ lib.optional (cfg.environmentFile != null) cfg.environmentFile;
233 ExecStart = lib.getExe vaultwarden;
234 LimitNOFILE = "1048576";
235 CapabilityBoundingSet = [ "" ];
236 DeviceAllow = [ "" ];
237 DevicePolicy = "closed";
238 LockPersonality = true;
239 MemoryDenyWriteExecute = true;
240 NoNewPrivileges = !useSendmail;
241 PrivateDevices = !useSendmail;
242 PrivateTmp = true;
243 PrivateUsers = !useSendmail;
244 ProcSubset = "pid";
245 ProtectClock = true;
246 ProtectControlGroups = true;
247 ProtectHome = true;
248 ProtectHostname = true;
249 ProtectKernelLogs = true;
250 ProtectKernelModules = true;
251 ProtectKernelTunables = true;
252 ProtectProc = "noaccess";
253 ProtectSystem = "strict";
254 RemoveIPC = true;
255 RestrictAddressFamilies = [
256 "AF_INET"
257 "AF_INET6"
258 "AF_UNIX"
259 ];
260 RestrictNamespaces = true;
261 RestrictRealtime = true;
262 RestrictSUIDSGID = true;
263 inherit StateDirectory;
264 StateDirectoryMode = "0700";
265 SystemCallArchitectures = "native";
266 SystemCallFilter =
267 [
268 "@system-service"
269 ]
270 ++ lib.optionals (!useSendmail) [
271 "~@privileged"
272 ];
273 Restart = "always";
274 UMask = "0077";
275 };
276 wantedBy = [ "multi-user.target" ];
277 };
278
279 systemd.services.backup-vaultwarden = lib.mkIf (cfg.backupDir != null) {
280 description = "Backup vaultwarden";
281 environment = {
282 DATA_FOLDER = dataDir;
283 BACKUP_FOLDER = cfg.backupDir;
284 };
285 path = with pkgs; [ sqlite ];
286 # if both services are started at the same time, vaultwarden fails with "database is locked"
287 before = [ "vaultwarden.service" ];
288 serviceConfig = {
289 SyslogIdentifier = "backup-vaultwarden";
290 Type = "oneshot";
291 User = lib.mkDefault user;
292 Group = lib.mkDefault group;
293 ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}";
294 };
295 wantedBy = [ "multi-user.target" ];
296 };
297
298 systemd.timers.backup-vaultwarden = lib.mkIf (cfg.backupDir != null) {
299 description = "Backup vaultwarden on time";
300 timerConfig = {
301 OnCalendar = lib.mkDefault "23:00";
302 Persistent = "true";
303 Unit = "backup-vaultwarden.service";
304 };
305 wantedBy = [ "multi-user.target" ];
306 };
307
308 systemd.tmpfiles.settings = lib.mkIf (cfg.backupDir != null) {
309 "10-vaultwarden".${cfg.backupDir}.d = {
310 inherit user group;
311 mode = "0770";
312 };
313 };
314 };
315
316 meta = {
317 # uses attributes of the linked package
318 buildDocsInSandbox = false;
319 maintainers = with lib.maintainers; [
320 dotlambda
321 SuperSandro2000
322 ];
323 };
324}