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; coercedTo path lib.singleton (listOf path);
176 default = [ ];
177 example = "/var/lib/vaultwarden.env";
178 description = ''
179 Additional environment file or files 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.mkPackageOption pkgs [ "vaultwarden" "webvault" ] { };
201 };
202
203 config = lib.mkIf cfg.enable {
204 assertions = [
205 {
206 assertion = cfg.backupDir != null -> cfg.dbBackend == "sqlite";
207 message = "Backups for database backends other than sqlite will need customization";
208 }
209 {
210 assertion = cfg.backupDir != null -> !(lib.hasPrefix dataDir cfg.backupDir);
211 message = "Backup directory can not be in ${dataDir}";
212 }
213 ];
214
215 users.users.vaultwarden = {
216 inherit group;
217 isSystemUser = true;
218 };
219 users.groups.vaultwarden = { };
220
221 systemd.services.vaultwarden = {
222 after = [ "network-online.target" ];
223 wants = [ "network-online.target" ];
224 path = with pkgs; [ openssl ];
225 serviceConfig = {
226 User = user;
227 Group = group;
228 EnvironmentFile = [ configFile ] ++ cfg.environmentFile;
229 ExecStart = lib.getExe vaultwarden;
230 LimitNOFILE = "1048576";
231 CapabilityBoundingSet = [ "" ];
232 DeviceAllow = [ "" ];
233 DevicePolicy = "closed";
234 LockPersonality = true;
235 MemoryDenyWriteExecute = true;
236 NoNewPrivileges = !useSendmail;
237 PrivateDevices = !useSendmail;
238 PrivateTmp = true;
239 PrivateUsers = !useSendmail;
240 ProcSubset = "pid";
241 ProtectClock = true;
242 ProtectControlGroups = true;
243 ProtectHome = true;
244 ProtectHostname = true;
245 ProtectKernelLogs = true;
246 ProtectKernelModules = true;
247 ProtectKernelTunables = true;
248 ProtectProc = "noaccess";
249 ProtectSystem = "strict";
250 RemoveIPC = true;
251 RestrictAddressFamilies = [
252 "AF_INET"
253 "AF_INET6"
254 "AF_UNIX"
255 ];
256 RestrictNamespaces = true;
257 RestrictRealtime = true;
258 RestrictSUIDSGID = true;
259 inherit StateDirectory;
260 StateDirectoryMode = "0700";
261 SystemCallArchitectures = "native";
262 SystemCallFilter = [
263 "@system-service"
264 ]
265 ++ lib.optionals (!useSendmail) [
266 "~@privileged"
267 ];
268 Restart = "always";
269 UMask = "0077";
270 };
271 wantedBy = [ "multi-user.target" ];
272 };
273
274 systemd.services.backup-vaultwarden = lib.mkIf (cfg.backupDir != null) {
275 description = "Backup vaultwarden";
276 environment = {
277 DATA_FOLDER = dataDir;
278 BACKUP_FOLDER = cfg.backupDir;
279 };
280 path = with pkgs; [ sqlite ];
281 # if both services are started at the same time, vaultwarden fails with "database is locked"
282 before = [ "vaultwarden.service" ];
283 serviceConfig = {
284 SyslogIdentifier = "backup-vaultwarden";
285 Type = "oneshot";
286 User = lib.mkDefault user;
287 Group = lib.mkDefault group;
288 ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}";
289 };
290 wantedBy = [ "multi-user.target" ];
291 };
292
293 systemd.timers.backup-vaultwarden = lib.mkIf (cfg.backupDir != null) {
294 description = "Backup vaultwarden on time";
295 timerConfig = {
296 OnCalendar = lib.mkDefault "23:00";
297 Persistent = "true";
298 Unit = "backup-vaultwarden.service";
299 };
300 wantedBy = [ "multi-user.target" ];
301 };
302
303 systemd.tmpfiles.settings = lib.mkIf (cfg.backupDir != null) {
304 "10-vaultwarden".${cfg.backupDir}.d = {
305 inherit user group;
306 mode = "0770";
307 };
308 };
309 };
310
311 meta = {
312 # uses attributes of the linked package
313 buildDocsInSandbox = false;
314 maintainers = with lib.maintainers; [
315 dotlambda
316 SuperSandro2000
317 ];
318 };
319}