1{ config, lib, options, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.vault;
7 opt = options.services.vault;
8
9 configFile = pkgs.writeText "vault.hcl" ''
10 # vault in dev mode will refuse to start if its configuration sets listener
11 ${lib.optionalString (!cfg.dev) ''
12 listener "tcp" {
13 address = "${cfg.address}"
14 ${if (cfg.tlsCertFile == null || cfg.tlsKeyFile == null) then ''
15 tls_disable = "true"
16 '' else ''
17 tls_cert_file = "${cfg.tlsCertFile}"
18 tls_key_file = "${cfg.tlsKeyFile}"
19 ''}
20 ${cfg.listenerExtraConfig}
21 }
22 ''}
23 storage "${cfg.storageBackend}" {
24 ${optionalString (cfg.storagePath != null) ''path = "${cfg.storagePath}"''}
25 ${optionalString (cfg.storageConfig != null) cfg.storageConfig}
26 }
27 ${optionalString (cfg.telemetryConfig != "") ''
28 telemetry {
29 ${cfg.telemetryConfig}
30 }
31 ''}
32 ${cfg.extraConfig}
33 '';
34
35 allConfigPaths = [configFile] ++ cfg.extraSettingsPaths;
36 configOptions = escapeShellArgs
37 (lib.optional cfg.dev "-dev" ++
38 lib.optional (cfg.dev && cfg.devRootTokenID != null) "-dev-root-token-id=${cfg.devRootTokenID}"
39 ++ (concatMap (p: ["-config" p]) allConfigPaths));
40
41in
42
43{
44 options = {
45 services.vault = {
46 enable = mkEnableOption (lib.mdDoc "Vault daemon");
47
48 package = mkOption {
49 type = types.package;
50 default = pkgs.vault;
51 defaultText = literalExpression "pkgs.vault";
52 description = lib.mdDoc "This option specifies the vault package to use.";
53 };
54
55 dev = mkOption {
56 type = types.bool;
57 default = false;
58 description = lib.mdDoc ''
59 In this mode, Vault runs in-memory and starts unsealed. This option is not meant production but for development and testing i.e. for nixos tests.
60 '';
61 };
62
63 devRootTokenID = mkOption {
64 type = types.str;
65 default = false;
66 description = lib.mdDoc ''
67 Initial root token. This only applies when {option}`services.vault.dev` is true
68 '';
69 };
70
71 address = mkOption {
72 type = types.str;
73 default = "127.0.0.1:8200";
74 description = lib.mdDoc "The name of the ip interface to listen to";
75 };
76
77 tlsCertFile = mkOption {
78 type = types.nullOr types.str;
79 default = null;
80 example = "/path/to/your/cert.pem";
81 description = lib.mdDoc "TLS certificate file. TLS will be disabled unless this option is set";
82 };
83
84 tlsKeyFile = mkOption {
85 type = types.nullOr types.str;
86 default = null;
87 example = "/path/to/your/key.pem";
88 description = lib.mdDoc "TLS private key file. TLS will be disabled unless this option is set";
89 };
90
91 listenerExtraConfig = mkOption {
92 type = types.lines;
93 default = ''
94 tls_min_version = "tls12"
95 '';
96 description = lib.mdDoc "Extra text appended to the listener section.";
97 };
98
99 storageBackend = mkOption {
100 type = types.enum [ "inmem" "file" "consul" "zookeeper" "s3" "azure" "dynamodb" "etcd" "mssql" "mysql" "postgresql" "swift" "gcs" "raft" ];
101 default = "inmem";
102 description = lib.mdDoc "The name of the type of storage backend";
103 };
104
105 storagePath = mkOption {
106 type = types.nullOr types.path;
107 default = if cfg.storageBackend == "file" || cfg.storageBackend == "raft" then "/var/lib/vault" else null;
108 defaultText = literalExpression ''
109 if config.${opt.storageBackend} == "file" || cfg.storageBackend == "raft"
110 then "/var/lib/vault"
111 else null
112 '';
113 description = lib.mdDoc "Data directory for file backend";
114 };
115
116 storageConfig = mkOption {
117 type = types.nullOr types.lines;
118 default = null;
119 description = lib.mdDoc ''
120 HCL configuration to insert in the storageBackend section.
121
122 Confidential values should not be specified here because this option's
123 value is written to the Nix store, which is publicly readable.
124 Provide credentials and such in a separate file using
125 [](#opt-services.vault.extraSettingsPaths).
126 '';
127 };
128
129 telemetryConfig = mkOption {
130 type = types.lines;
131 default = "";
132 description = lib.mdDoc "Telemetry configuration";
133 };
134
135 extraConfig = mkOption {
136 type = types.lines;
137 default = "";
138 description = lib.mdDoc "Extra text appended to {file}`vault.hcl`.";
139 };
140
141 extraSettingsPaths = mkOption {
142 type = types.listOf types.path;
143 default = [];
144 description = lib.mdDoc ''
145 Configuration files to load besides the immutable one defined by the NixOS module.
146 This can be used to avoid putting credentials in the Nix store, which can be read by any user.
147
148 Each path can point to a JSON- or HCL-formatted file, or a directory
149 to be scanned for files with `.hcl` or
150 `.json` extensions.
151
152 To upload the confidential file with NixOps, use for example:
153
154 ```
155 # https://releases.nixos.org/nixops/latest/manual/manual.html#opt-deployment.keys
156 deployment.keys."vault.hcl" = let db = import ./db-credentials.nix; in {
157 text = ${"''"}
158 storage "postgresql" {
159 connection_url = "postgres://''${db.username}:''${db.password}@host.example.com/exampledb?sslmode=verify-ca"
160 }
161 ${"''"};
162 user = "vault";
163 };
164 services.vault.extraSettingsPaths = ["/run/keys/vault.hcl"];
165 services.vault.storageBackend = "postgresql";
166 users.users.vault.extraGroups = ["keys"];
167 ```
168 '';
169 };
170 };
171 };
172
173 config = mkIf cfg.enable {
174 assertions = [
175 {
176 assertion = cfg.storageBackend == "inmem" -> (cfg.storagePath == null && cfg.storageConfig == null);
177 message = ''The "inmem" storage expects no services.vault.storagePath nor services.vault.storageConfig'';
178 }
179 {
180 assertion = (
181 (cfg.storageBackend == "file" -> (cfg.storagePath != null && cfg.storageConfig == null)) &&
182 (cfg.storagePath != null -> (cfg.storageBackend == "file" || cfg.storageBackend == "raft"))
183 );
184 message = ''You must set services.vault.storagePath only when using the "file" or "raft" backend'';
185 }
186 ];
187
188 users.users.vault = {
189 name = "vault";
190 group = "vault";
191 uid = config.ids.uids.vault;
192 description = "Vault daemon user";
193 };
194 users.groups.vault.gid = config.ids.gids.vault;
195
196 systemd.tmpfiles.rules = optional (cfg.storagePath != null)
197 "d '${cfg.storagePath}' 0700 vault vault - -";
198
199 systemd.services.vault = {
200 description = "Vault server daemon";
201
202 wantedBy = ["multi-user.target"];
203 after = [ "network.target" ]
204 ++ optional (config.services.consul.enable && cfg.storageBackend == "consul") "consul.service";
205
206 restartIfChanged = false; # do not restart on "nixos-rebuild switch". It would seal the storage and disrupt the clients.
207
208 startLimitIntervalSec = 60;
209 startLimitBurst = 3;
210 serviceConfig = {
211 User = "vault";
212 Group = "vault";
213 ExecStart = "${cfg.package}/bin/vault server ${configOptions}";
214 ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
215 StateDirectory = "vault";
216 # In `dev` mode vault will put its token here
217 Environment = lib.optional (cfg.dev) "HOME=/var/lib/vault";
218 PrivateDevices = true;
219 PrivateTmp = true;
220 ProtectSystem = "full";
221 ProtectHome = "read-only";
222 AmbientCapabilities = "cap_ipc_lock";
223 NoNewPrivileges = true;
224 KillSignal = "SIGINT";
225 TimeoutStopSec = "30s";
226 Restart = "on-failure";
227 };
228
229 unitConfig.RequiresMountsFor = optional (cfg.storagePath != null) cfg.storagePath;
230 };
231 };
232
233}