at master 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; 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}