1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.caddy;
12
13 certs = config.security.acme.certs;
14 virtualHosts = attrValues cfg.virtualHosts;
15 acmeEnabledVhosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts;
16 vhostCertNames = unique (map (hostOpts: hostOpts.useACMEHost) acmeEnabledVhosts);
17 dependentCertNames = filter (cert: certs.${cert}.dnsProvider == null) vhostCertNames; # those that might depend on the HTTP server
18 independentCertNames = filter (cert: certs.${cert}.dnsProvider != null) vhostCertNames; # those that don't depend on the HTTP server
19
20 mkVHostConf =
21 hostOpts:
22 let
23 sslCertDir = config.security.acme.certs.${hostOpts.useACMEHost}.directory;
24 in
25 ''
26 ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} {
27 ${optionalString (
28 hostOpts.listenAddresses != [ ]
29 ) "bind ${concatStringsSep " " hostOpts.listenAddresses}"}
30 ${optionalString (
31 hostOpts.useACMEHost != null
32 ) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"}
33 log {
34 ${hostOpts.logFormat}
35 }
36
37 ${hostOpts.extraConfig}
38 }
39 '';
40
41 settingsFormat = pkgs.formats.json { };
42
43 configFile =
44 if cfg.settings != { } then
45 settingsFormat.generate "caddy.json" cfg.settings
46 else
47 let
48 Caddyfile = pkgs.writeTextDir "Caddyfile" ''
49 {
50 ${cfg.globalConfig}
51 }
52 ${cfg.extraConfig}
53 ${concatMapStringsSep "\n" mkVHostConf virtualHosts}
54 '';
55
56 Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { } ''
57 mkdir -p $out
58 cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile
59 ${lib.getExe cfg.package} fmt --overwrite $out/Caddyfile
60 '';
61 in
62 "${
63 if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile
64 }/Caddyfile";
65
66 etcConfigFile = "caddy/caddy_config";
67
68 configPath = "/etc/${etcConfigFile}";
69
70 mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
71in
72{
73 imports = [
74 (mkRemovedOptionModule [
75 "services"
76 "caddy"
77 "agree"
78 ] "this option is no longer necessary for Caddy 2")
79 (mkRenamedOptionModule [ "services" "caddy" "ca" ] [ "services" "caddy" "acmeCA" ])
80 (mkRenamedOptionModule [ "services" "caddy" "config" ] [ "services" "caddy" "extraConfig" ])
81 ];
82
83 # interface
84 options.services.caddy = {
85 enable = mkEnableOption "Caddy web server";
86
87 user = mkOption {
88 default = "caddy";
89 type = types.str;
90 description = ''
91 User account under which caddy runs.
92
93 ::: {.note}
94 If left as the default value this user will automatically be created
95 on system activation, otherwise you are responsible for
96 ensuring the user exists before the Caddy service starts.
97 :::
98 '';
99 };
100
101 group = mkOption {
102 default = "caddy";
103 type = types.str;
104 description = ''
105 Group under which caddy runs.
106
107 ::: {.note}
108 If left as the default value this group will automatically be created
109 on system activation, otherwise you are responsible for
110 ensuring the group exists before the Caddy service starts.
111 :::
112 '';
113 };
114
115 package = mkPackageOption pkgs "caddy" { };
116
117 dataDir = mkOption {
118 type = types.path;
119 default = "/var/lib/caddy";
120 description = ''
121 The data directory for caddy.
122
123 ::: {.note}
124 If left as the default value this directory will automatically be created
125 before the Caddy server starts, otherwise you are responsible for ensuring
126 the directory exists with appropriate ownership and permissions.
127
128 Caddy v2 replaced `CADDYPATH` with XDG directories.
129 See <https://caddyserver.com/docs/conventions#file-locations>.
130 :::
131 '';
132 };
133
134 logDir = mkOption {
135 type = types.path;
136 default = "/var/log/caddy";
137 description = ''
138 Directory for storing Caddy access logs.
139
140 ::: {.note}
141 If left as the default value this directory will automatically be created
142 before the Caddy server starts, otherwise the sysadmin is responsible for
143 ensuring the directory exists with appropriate ownership and permissions.
144 :::
145 '';
146 };
147
148 logFormat = mkOption {
149 type = types.lines;
150 default = ''
151 level ERROR
152 '';
153 example = literalExpression ''
154 mkForce "level INFO";
155 '';
156 description = ''
157 Configuration for the default logger. See
158 <https://caddyserver.com/docs/caddyfile/options#log>
159 for details.
160 '';
161 };
162
163 configFile = mkOption {
164 type = types.path;
165 default = configFile;
166 defaultText = "A Caddyfile automatically generated by values from services.caddy.*";
167 example = literalExpression ''
168 pkgs.writeText "Caddyfile" '''
169 example.com
170
171 root * /var/www/wordpress
172 php_fastcgi unix//run/php/php-version-fpm.sock
173 file_server
174 ''';
175 '';
176 description = ''
177 Override the configuration file used by Caddy. By default,
178 NixOS generates one automatically.
179
180 The configuration file is exposed at {file}`${configPath}`.
181 '';
182 };
183
184 adapter = mkOption {
185 default =
186 if ((cfg.configFile != configFile) || (builtins.baseNameOf cfg.configFile) == "Caddyfile") then
187 "caddyfile"
188 else
189 null;
190 defaultText = literalExpression ''
191 if ((cfg.configFile != configFile) || (builtins.baseNameOf cfg.configFile) == "Caddyfile") then "caddyfile" else null
192 '';
193 example = literalExpression "nginx";
194 type = with types; nullOr str;
195 description = ''
196 Name of the config adapter to use.
197 See <https://caddyserver.com/docs/config-adapters>
198 for the full list.
199
200 If `null` is specified, the `--adapter` argument is omitted when
201 starting or restarting Caddy. Notably, this allows specification of a
202 configuration file in Caddy's native JSON format, as long as the
203 filename does not start with `Caddyfile` (in which case the `caddyfile`
204 adapter is implicitly enabled). See
205 <https://caddyserver.com/docs/command-line#caddy-run> for details.
206
207 ::: {.note}
208 Any value other than `null` or `caddyfile` is only valid when providing
209 your own `configFile`.
210 :::
211 '';
212 };
213
214 resume = mkOption {
215 default = false;
216 type = types.bool;
217 description = ''
218 Use saved config, if any (and prefer over any specified configuration passed with `--config`).
219 '';
220 };
221
222 globalConfig = mkOption {
223 type = types.lines;
224 default = "";
225 example = ''
226 debug
227 servers {
228 protocol {
229 experimental_http3
230 }
231 }
232 '';
233 description = ''
234 Additional lines of configuration appended to the global config section
235 of the `Caddyfile`.
236
237 Refer to <https://caddyserver.com/docs/caddyfile/options#global-options>
238 for details on supported values.
239 '';
240 };
241
242 extraConfig = mkOption {
243 type = types.lines;
244 default = "";
245 example = ''
246 example.com {
247 encode gzip
248 log
249 root /srv/http
250 }
251 '';
252 description = ''
253 Additional lines of configuration appended to the automatically
254 generated `Caddyfile`.
255 '';
256 };
257
258 virtualHosts = mkOption {
259 type = with types; attrsOf (submodule (import ./vhost-options.nix { inherit cfg; }));
260 default = { };
261 example = literalExpression ''
262 {
263 "hydra.example.com" = {
264 serverAliases = [ "www.hydra.example.com" ];
265 extraConfig = '''
266 encode gzip
267 root * /srv/http
268 ''';
269 };
270 };
271 '';
272 description = ''
273 Declarative specification of virtual hosts served by Caddy.
274 '';
275 };
276
277 acmeCA = mkOption {
278 default = null;
279 example = "https://acme-v02.api.letsencrypt.org/directory";
280 type = with types; nullOr str;
281 description = ''
282 ::: {.note}
283 Sets the [`acme_ca` option](https://caddyserver.com/docs/caddyfile/options#acme-ca)
284 in the global options block of the resulting Caddyfile.
285 :::
286
287 The URL to the ACME CA's directory. It is strongly recommended to set
288 this to `https://acme-staging-v02.api.letsencrypt.org/directory` for
289 Let's Encrypt's [staging endpoint](https://letsencrypt.org/docs/staging-environment/)
290 while testing or in development.
291
292 Value `null` should be prefered for production setups,
293 as it omits the `acme_ca` option to enable
294 [automatic issuer fallback](https://caddyserver.com/docs/automatic-https#issuer-fallback).
295 '';
296 };
297
298 email = mkOption {
299 default = null;
300 type = with types; nullOr str;
301 description = ''
302 Your email address. Mainly used when creating an ACME account with your
303 CA, and is highly recommended in case there are problems with your
304 certificates.
305 '';
306 };
307
308 enableReload = mkOption {
309 default = true;
310 type = types.bool;
311 description = ''
312 Reload Caddy instead of restarting it when configuration file changes.
313
314 Note that enabling this option requires the [admin API](https://caddyserver.com/docs/caddyfile/options#admin)
315 to not be turned off.
316
317 If you enable this option, consider setting [`grace_period`](https://caddyserver.com/docs/caddyfile/options#grace-period)
318 to a non-infinite value in {option}`services.caddy.globalConfig`
319 to prevent Caddy waiting for active connections to finish,
320 which could delay the reload essentially indefinitely.
321 '';
322 };
323
324 settings = mkOption {
325 type = settingsFormat.type;
326 default = { };
327 description = ''
328 Structured configuration for Caddy to generate a Caddy JSON configuration file.
329 See <https://caddyserver.com/docs/json/> for available options.
330
331 ::: {.warning}
332 Using a [Caddyfile](https://caddyserver.com/docs/caddyfile) instead of a JSON config is highly recommended by upstream.
333 There are only very few exception to this.
334
335 Please use a Caddyfile via {option}`services.caddy.configFile`, {option}`services.caddy.virtualHosts` or
336 {option}`services.caddy.extraConfig` with {option}`services.caddy.globalConfig` instead.
337 :::
338
339 ::: {.note}
340 Takes presence over most `services.caddy.*` options, such as {option}`services.caddy.configFile` and {option}`services.caddy.virtualHosts`, if specified.
341 :::
342 '';
343 };
344
345 environmentFile = mkOption {
346 type = with types; nullOr path;
347 default = null;
348 example = "/run/secrets/caddy.env";
349 description = ''
350 Environment file as defined in {manpage}`systemd.exec(5)`.
351
352 You can use environment variables to pass secrets to the service without adding
353 them to the world-redable nix store.
354
355 ```
356 # in configuration.nix
357 services.caddy.environmentFile = "/run/secrets/caddy.env";
358 services.caddy.globalConfig = '''
359 {
360 acme_ca https://acme.zerossl.com/v2/DV90
361 acme_eab {
362 key_id {$EAB_KEY_ID}
363 mac_key {$EAB_MAC_KEY}
364 }
365 }
366 ''';
367 ```
368
369 ```
370 # in /run/secrets/caddy.env
371 EAB_KEY_ID=secret
372 EAB_MAC_KEY=secret
373 ```
374
375 Find more examples
376 [here](https://caddyserver.com/docs/caddyfile/concepts#environment-variables)
377 '';
378 };
379 };
380
381 # implementation
382 config = mkIf cfg.enable {
383
384 assertions =
385 [
386 {
387 assertion = cfg.configFile == configFile -> cfg.adapter == "caddyfile" || cfg.adapter == null;
388 message = "To specify an adapter other than 'caddyfile' please provide your own configuration via `services.caddy.configFile`";
389 }
390 ]
391 ++ map (
392 name:
393 mkCertOwnershipAssertion {
394 cert = config.security.acme.certs.${name};
395 groups = config.users.groups;
396 services = [ config.systemd.services.caddy ];
397 }
398 ) vhostCertNames;
399
400 services.caddy.globalConfig = ''
401 ${optionalString (cfg.email != null) "email ${cfg.email}"}
402 ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"}
403 log {
404 ${cfg.logFormat}
405 }
406 '';
407
408 # https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes
409 boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
410 boot.kernel.sysctl."net.core.wmem_max" = mkDefault 2500000;
411
412 systemd.packages = [ cfg.package ];
413 systemd.services.caddy = {
414 wants = map (certName: "acme-finished-${certName}.target") vhostCertNames;
415 after =
416 map (certName: "acme-selfsigned-${certName}.service") vhostCertNames
417 ++ map (certName: "acme-${certName}.service") independentCertNames; # avoid loading self-signed key w/ real cert, or vice-versa
418 before = map (certName: "acme-${certName}.service") dependentCertNames;
419
420 wantedBy = [ "multi-user.target" ];
421 startLimitIntervalSec = 14400;
422 startLimitBurst = 10;
423 reloadTriggers = optional cfg.enableReload cfg.configFile;
424 restartTriggers = optional (!cfg.enableReload) cfg.configFile;
425
426 serviceConfig =
427 let
428 runOptions = ''--config ${configPath} ${
429 optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"
430 }'';
431 in
432 {
433 # Override the `ExecStart` line from upstream's systemd unit file by our own:
434 # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart=
435 # If the empty string is assigned to this option, the list of commands to start is reset, prior assignments of this option will have no effect.
436 ExecStart = [
437 ""
438 ''${lib.getExe cfg.package} run ${runOptions} ${optionalString cfg.resume "--resume"}''
439 ];
440 # Validating the configuration before applying it ensures we’ll get a proper error that will be reported when switching to the configuration
441 ExecReload = [
442 ""
443 ] ++ lib.optional cfg.enableReload "${lib.getExe cfg.package} reload ${runOptions} --force";
444 User = cfg.user;
445 Group = cfg.group;
446 ReadWritePaths = [ cfg.dataDir ];
447 StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ];
448 LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ];
449 Restart = "on-failure";
450 RestartPreventExitStatus = 1;
451 RestartSec = "5s";
452 EnvironmentFile = optional (cfg.environmentFile != null) cfg.environmentFile;
453
454 # TODO: attempt to upstream these options
455 NoNewPrivileges = true;
456 PrivateDevices = true;
457 ProtectHome = true;
458 };
459 };
460
461 users.users = optionalAttrs (cfg.user == "caddy") {
462 caddy = {
463 group = cfg.group;
464 uid = config.ids.uids.caddy;
465 home = cfg.dataDir;
466 };
467 };
468
469 users.groups = optionalAttrs (cfg.group == "caddy") {
470 caddy.gid = config.ids.gids.caddy;
471 };
472
473 security.acme.certs =
474 let
475 certCfg = map (
476 certName:
477 nameValuePair certName {
478 group = mkDefault cfg.group;
479 reloadServices = [ "caddy.service" ];
480 }
481 ) vhostCertNames;
482 in
483 listToAttrs certCfg;
484
485 environment.etc.${etcConfigFile}.source = cfg.configFile;
486 };
487}