grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / security / vaultwarden / default.nix
blob11d49b580b310e27b98361faeafd0b5b3a4e4c32
1 { config, lib, pkgs, ... }:
3 let
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).
13   nameToEnvVar = name:
14     let
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.
25   configEnv =
26     let
27       configEnv = lib.concatMapAttrs (name: value: lib.optionalAttrs (value != null) {
28         ${nameToEnvVar name} = if lib.isBool value then lib.boolToString value else toString value;
29       }) cfg.config;
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";
32     } // configEnv;
34   configFile = pkgs.writeText "vaultwarden.env" (lib.concatStrings (lib.mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
36   vaultwarden = cfg.package.override { inherit (cfg) dbBackend; };
38 in {
39   imports = [
40     (lib.mkRenamedOptionModule [ "services" "bitwarden_rs" ] [ "services" "vaultwarden" ])
41   ];
43   options.services.vaultwarden = {
44     enable = lib.mkEnableOption "vaultwarden";
46     dbBackend = lib.mkOption {
47       type = lib.types.enum [ "sqlite" "mysql" "postgresql" ];
48       default = "sqlite";
49       description = ''
50         Which database backend vaultwarden will be using.
51       '';
52     };
54     backupDir = lib.mkOption {
55       type = with lib.types; nullOr str;
56       default = null;
57       description = ''
58         The directory under which vaultwarden will backup its persistent data.
59       '';
60       example = "/var/backup/vaultwarden";
61     };
63     config = lib.mkOption {
64       type = with lib.types; attrsOf (nullOr (oneOf [ bool int str ]));
65       default = {
66         ROCKET_ADDRESS = "::1"; # default to localhost
67         ROCKET_PORT = 8222;
68       };
69       example = lib.literalExpression ''
70         {
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.
79           #
80           # A suitable NixOS nginx reverse proxy example config might be:
81           #
82           #     services.nginx.virtualHosts."bitwarden.example.com" = {
83           #       enableACME = true;
84           #       forceSSL = true;
85           #       locations."/" = {
86           #         proxyPass = "http://127.0.0.1:''${toString config.services.vaultwarden.config.ROCKET_PORT}";
87           #       };
88           #     };
89           ROCKET_ADDRESS = "127.0.0.1";
90           ROCKET_PORT = 8222;
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";
99           SMTP_PORT = 25;
100           SMTP_SSL = false;
102           SMTP_FROM = "admin@bitwarden.example.com";
103           SMTP_FROM_NAME = "example.com Bitwarden server";
104         }
105       '';
106       description = ''
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.
127       '';
128     };
130     environmentFile = lib.mkOption {
131       type = with lib.types; nullOr path;
132       default = null;
133       example = "/var/lib/vaultwarden.env";
134       description = ''
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):
146         ```
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...
150         ```
151       '';
152     };
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.";
161     };
162   };
164   config = lib.mkIf cfg.enable {
165     assertions = [
166       {
167         assertion = cfg.backupDir != null -> cfg.dbBackend == "sqlite";
168         message = "Backups for database backends other than sqlite will need customization";
169       }
170       {
171         assertion = cfg.backupDir != null -> !(lib.hasPrefix dataDir cfg.backupDir);
172         message = "Backup directory can not be in ${dataDir}";
173       }
174     ];
176     users.users.vaultwarden = {
177       inherit group;
178       isSystemUser = true;
179     };
180     users.groups.vaultwarden = { };
182     systemd.services.vaultwarden = {
183       after = [ "network.target" ];
184       path = with pkgs; [ openssl ];
185       serviceConfig = {
186         User = user;
187         Group = group;
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;
198         PrivateTmp = true;
199         PrivateUsers = true;
200         ProcSubset = "pid";
201         ProtectClock = true;
202         ProtectControlGroups = true;
203         ProtectHome = true;
204         ProtectHostname = true;
205         ProtectKernelLogs = true;
206         ProtectKernelModules = true;
207         ProtectKernelTunables = true;
208         ProtectProc = "noaccess";
209         ProtectSystem = "strict";
210         RemoveIPC = true;
211         RestrictAddressFamilies = [
212           "AF_INET"
213           "AF_INET6"
214           "AF_UNIX"
215         ];
216         RestrictNamespaces = true;
217         RestrictRealtime = true;
218         RestrictSUIDSGID = true;
219         inherit StateDirectory;
220         StateDirectoryMode = "0700";
221         SystemCallArchitectures = "native";
222         SystemCallFilter = [
223           "@system-service"
224           "~@privileged"
225         ];
226         Restart = "always";
227         UMask = "0077";
228       };
229       wantedBy = [ "multi-user.target" ];
230     };
232     systemd.services.backup-vaultwarden = lib.mkIf (cfg.backupDir != null) {
233       description = "Backup vaultwarden";
234       environment = {
235         DATA_FOLDER = dataDir;
236         BACKUP_FOLDER = cfg.backupDir;
237       };
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" ];
241       serviceConfig = {
242         SyslogIdentifier = "backup-vaultwarden";
243         Type = "oneshot";
244         User = lib.mkDefault user;
245         Group = lib.mkDefault group;
246         ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}";
247       };
248       wantedBy = [ "multi-user.target" ];
249     };
251     systemd.timers.backup-vaultwarden = lib.mkIf (cfg.backupDir != null) {
252       description = "Backup vaultwarden on time";
253       timerConfig = {
254         OnCalendar = lib.mkDefault "23:00";
255         Persistent = "true";
256         Unit = "backup-vaultwarden.service";
257       };
258       wantedBy = [ "multi-user.target" ];
259     };
261     systemd.tmpfiles.settings = lib.mkIf (cfg.backupDir != null) {
262       "10-vaultwarden".${cfg.backupDir}.d = {
263         inherit user group;
264         mode = "0770";
265       };
266     };
267   };
269   meta = {
270     # uses attributes of the linked package
271     buildDocsInSandbox = false;
272     maintainers = with lib.maintainers; [ dotlambda SuperSandro2000 ];
273   };