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