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