1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.caddy;
7
8 virtualHosts = attrValues cfg.virtualHosts;
9 acmeVHosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts;
10
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 }
22
23 ${hostOpts.extraConfig}
24 }
25 '';
26
27 settingsFormat = pkgs.formats.json { };
28
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 '';
41
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";
49
50 etcConfigFile = "caddy/caddy_config";
51
52 configPath = "/etc/${etcConfigFile}";
53
54 acmeHosts = unique (catAttrs "useACMEHost" acmeVHosts);
55
56 mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
57in
58{
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 ];
64
65 # interface
66 options.services.caddy = {
67 enable = mkEnableOption (lib.mdDoc "Caddy web server");
68
69 user = mkOption {
70 default = "caddy";
71 type = types.str;
72 description = lib.mdDoc ''
73 User account under which caddy runs.
74
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 };
82
83 group = mkOption {
84 default = "caddy";
85 type = types.str;
86 description = lib.mdDoc ''
87 Group account under which caddy runs.
88
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 };
96
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 };
105
106 dataDir = mkOption {
107 type = types.path;
108 default = "/var/lib/caddy";
109 description = lib.mdDoc ''
110 The data directory for caddy.
111
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.
116
117 Caddy v2 replaced `CADDYPATH` with XDG directories.
118 See <https://caddyserver.com/docs/conventions#file-locations>.
119 :::
120 '';
121 };
122
123 logDir = mkOption {
124 type = types.path;
125 default = "/var/log/caddy";
126 description = lib.mdDoc ''
127 Directory for storing Caddy access logs.
128
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 };
136
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 };
151
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
159
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.
168
169 The configuration file is exposed at {file}`${configPath}`.
170 '';
171 };
172
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.
184
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.
191
192 ::: {.note}
193 Any value other than `null` or `caddyfile` is only valid when providing
194 your own `configFile`.
195 :::
196 '';
197 };
198
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 };
206
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`.
221
222 Refer to <https://caddyserver.com/docs/caddyfile/options#global-options>
223 for details on supported values.
224 '';
225 };
226
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 };
242
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 };
261
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 :::
271
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.
276
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 };
282
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 };
292
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.
298
299 Note that enabling this option requires the [admin API](https://caddyserver.com/docs/caddyfile/options#admin)
300 to not be turned off.
301
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 };
308
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.
315
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.
319
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 :::
323
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 };
330
331 # implementation
332 config = mkIf cfg.enable {
333
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;
343
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 '';
351
352 # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
353 boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
354
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;
360
361 wantedBy = [ "multi-user.target" ];
362 startLimitIntervalSec = 14400;
363 startLimitBurst = 10;
364 reloadTriggers = optional cfg.enableReload cfg.configFile;
365
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 RestartSecs = "5s";
382
383 # TODO: attempt to upstream these options
384 NoNewPrivileges = true;
385 PrivateDevices = true;
386 ProtectHome = true;
387 };
388 };
389
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 };
397
398 users.groups = optionalAttrs (cfg.group == "caddy") {
399 caddy.gid = config.ids.gids.caddy;
400 };
401
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;
410
411 environment.etc.${etcConfigFile}.source = cfg.configFile;
412 };
413}