summary refs log tree commit diff
path: root/nixos/modules/services/security/bitwarden_rs/default.nix
blob: 903a532703774dc61f12091450725367db6a6997 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
{ config, lib, pkgs, ... }:

with lib;

let
  cfg = config.services.bitwarden_rs;
  user = config.users.users.bitwarden_rs.name;
  group = config.users.groups.bitwarden_rs.name;

  # Convert name from camel case (e.g. disable2FARemember) to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
  nameToEnvVar = name:
    let
      parts = builtins.split "([A-Z0-9]+)" name;
      partsToEnvVar = parts: foldl' (key: x: let last = stringLength key - 1; in
        if isList x then key + optionalString (key != "" && substring last 1 key != "_") "_" + head x
        else if key != "" && elem (substring 0 1 x) lowerChars then # to handle e.g. [ "disable" [ "2FAR" ] "emember" ]
          substring 0 last key + optionalString (substring (last - 1) 1 key != "_") "_" + substring last 1 key + toUpper x
        else key + toUpper x) "" parts;
    in if builtins.match "[A-Z0-9_]+" name != null then name else partsToEnvVar parts;

  # Due to the different naming schemes allowed for config keys,
  # we can only check for values consistently after converting them to their corresponding environment variable name.
  configEnv =
    let
      configEnv = listToAttrs (concatLists (mapAttrsToList (name: value:
        if value != null then [ (nameValuePair (nameToEnvVar name) (if isBool value then boolToString value else toString value)) ] else []
      ) cfg.config));
    in { DATA_FOLDER = "/var/lib/bitwarden_rs"; } // optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
      WEB_VAULT_FOLDER = "${pkgs.bitwarden_rs-vault}/share/bitwarden_rs/vault";
    } // configEnv;

  configFile = pkgs.writeText "bitwarden_rs.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv));

  bitwarden_rs = pkgs.bitwarden_rs.override { inherit (cfg) dbBackend; };

in {
  options.services.bitwarden_rs = with types; {
    enable = mkEnableOption "bitwarden_rs";

    dbBackend = mkOption {
      type = enum [ "sqlite" "mysql" "postgresql" ];
      default = "sqlite";
      description = ''
        Which database backend bitwarden_rs will be using.
      '';
    };

    backupDir = mkOption {
      type = nullOr str;
      default = null;
      description = ''
        The directory under which bitwarden_rs will backup its persistent data.
      '';
    };

    config = mkOption {
      type = attrsOf (nullOr (oneOf [ bool int str ]));
      default = {};
      example = literalExample ''
        {
          domain = "https://bw.domain.tld:8443";
          signupsAllowed = true;
          rocketPort = 8222;
          rocketLog = "critical";
        }
      '';
      description = ''
        The configuration of bitwarden_rs is done through environment variables,
        therefore the names are converted from camel case (e.g. disable2FARemember)
        to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
        In this conversion digits (0-9) are handled just like upper case characters,
        so foo2 would be converted to FOO_2.
        Names already in this format remain unchanged, so FOO2 remains FOO2 if passed as such,
        even though foo2 would have been converted to FOO_2.
        This allows working around any potential future conflicting naming conventions.

        Based on the attributes passed to this config option an environment file will be generated
        that is passed to bitwarden_rs's systemd service.

        The available configuration options can be found in
        <link xlink:href="https://github.com/dani-garcia/bitwarden_rs/blob/${bitwarden_rs.version}/.env.template">the environment template file</link>.
      '';
    };
  };

  config = mkIf cfg.enable {
    assertions = [ {
      assertion = cfg.backupDir != null -> cfg.dbBackend == "sqlite";
      message = "Backups for database backends other than sqlite will need customization";
    } ];

    users.users.bitwarden_rs = {
      inherit group;
      isSystemUser = true;
    };
    users.groups.bitwarden_rs = { };

    systemd.services.bitwarden_rs = {
      after = [ "network.target" ];
      path = with pkgs; [ openssl ];
      serviceConfig = {
        User = user;
        Group = group;
        EnvironmentFile = configFile;
        ExecStart = "${bitwarden_rs}/bin/bitwarden_rs";
        LimitNOFILE = "1048576";
        LimitNPROC = "64";
        PrivateTmp = "true";
        PrivateDevices = "true";
        ProtectHome = "true";
        ProtectSystem = "strict";
        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
        StateDirectory = "bitwarden_rs";
      };
      wantedBy = [ "multi-user.target" ];
    };

    systemd.services.backup-bitwarden_rs = mkIf (cfg.backupDir != null) {
      description = "Backup bitwarden_rs";
      environment = {
        DATA_FOLDER = "/var/lib/bitwarden_rs";
        BACKUP_FOLDER = cfg.backupDir;
      };
      path = with pkgs; [ sqlite ];
      serviceConfig = {
        SyslogIdentifier = "backup-bitwarden_rs";
        Type = "oneshot";
        User = mkDefault user;
        Group = mkDefault group;
        ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}";
      };
      wantedBy = [ "multi-user.target" ];
    };

    systemd.timers.backup-bitwarden_rs = mkIf (cfg.backupDir != null) {
      description = "Backup bitwarden_rs on time";
      timerConfig = {
        OnCalendar = mkDefault "23:00";
        Persistent = "true";
        Unit = "backup-bitwarden_rs.service";
      };
      wantedBy = [ "multi-user.target" ];
    };
  };
}