at 25.11-pre 11 kB view raw
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}