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 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 configFile =
28 let
29 Caddyfile = pkgs.writeTextDir "Caddyfile" ''
30 {
31 ${cfg.globalConfig}
32 }
33 ${cfg.extraConfig}
34 '';
35
36 Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } ''
37 mkdir -p $out
38 cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile
39 caddy fmt --overwrite $out/Caddyfile
40 '';
41 in
42 "${if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile}/Caddyfile";
43
44 acmeHosts = unique (catAttrs "useACMEHost" acmeVHosts);
45
46 mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
47in
48{
49 imports = [
50 (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2")
51 (mkRenamedOptionModule [ "services" "caddy" "ca" ] [ "services" "caddy" "acmeCA" ])
52 (mkRenamedOptionModule [ "services" "caddy" "config" ] [ "services" "caddy" "extraConfig" ])
53 ];
54
55 # interface
56 options.services.caddy = {
57 enable = mkEnableOption (lib.mdDoc "Caddy web server");
58
59 user = mkOption {
60 default = "caddy";
61 type = types.str;
62 description = lib.mdDoc ''
63 User account under which caddy runs.
64
65 ::: {.note}
66 If left as the default value this user will automatically be created
67 on system activation, otherwise you are responsible for
68 ensuring the user exists before the Caddy service starts.
69 :::
70 '';
71 };
72
73 group = mkOption {
74 default = "caddy";
75 type = types.str;
76 description = lib.mdDoc ''
77 Group account under which caddy runs.
78
79 ::: {.note}
80 If left as the default value this user will automatically be created
81 on system activation, otherwise you are responsible for
82 ensuring the user exists before the Caddy service starts.
83 :::
84 '';
85 };
86
87 package = mkOption {
88 default = pkgs.caddy;
89 defaultText = literalExpression "pkgs.caddy";
90 type = types.package;
91 description = lib.mdDoc ''
92 Caddy package to use.
93 '';
94 };
95
96 dataDir = mkOption {
97 type = types.path;
98 default = "/var/lib/caddy";
99 description = lib.mdDoc ''
100 The data directory for caddy.
101
102 ::: {.note}
103 If left as the default value this directory will automatically be created
104 before the Caddy server starts, otherwise you are responsible for ensuring
105 the directory exists with appropriate ownership and permissions.
106
107 Caddy v2 replaced `CADDYPATH` with XDG directories.
108 See <https://caddyserver.com/docs/conventions#file-locations>.
109 :::
110 '';
111 };
112
113 logDir = mkOption {
114 type = types.path;
115 default = "/var/log/caddy";
116 description = lib.mdDoc ''
117 Directory for storing Caddy access logs.
118
119 ::: {.note}
120 If left as the default value this directory will automatically be created
121 before the Caddy server starts, otherwise the sysadmin is responsible for
122 ensuring the directory exists with appropriate ownership and permissions.
123 :::
124 '';
125 };
126
127 logFormat = mkOption {
128 type = types.lines;
129 default = ''
130 level ERROR
131 '';
132 example = literalExpression ''
133 mkForce "level INFO";
134 '';
135 description = lib.mdDoc ''
136 Configuration for the default logger. See
137 <https://caddyserver.com/docs/caddyfile/options#log>
138 for details.
139 '';
140 };
141
142 configFile = mkOption {
143 type = types.path;
144 default = configFile;
145 defaultText = "A Caddyfile automatically generated by values from services.caddy.*";
146 example = literalExpression ''
147 pkgs.writeTextDir "Caddyfile" '''
148 example.com
149
150 root * /var/www/wordpress
151 php_fastcgi unix//run/php/php-version-fpm.sock
152 file_server
153 ''';
154 '';
155 description = lib.mdDoc ''
156 Override the configuration file used by Caddy. By default,
157 NixOS generates one automatically.
158 '';
159 };
160
161 adapter = mkOption {
162 default = null;
163 example = literalExpression "nginx";
164 type = with types; nullOr str;
165 description = lib.mdDoc ''
166 Name of the config adapter to use.
167 See <https://caddyserver.com/docs/config-adapters>
168 for the full list.
169
170 If `null` is specified, the `--adapter` argument is omitted when
171 starting or restarting Caddy. Notably, this allows specification of a
172 configuration file in Caddy's native JSON format, as long as the
173 filename does not start with `Caddyfile` (in which case the `caddyfile`
174 adapter is implicitly enabled). See
175 <https://caddyserver.com/docs/command-line#caddy-run> for details.
176
177 ::: {.note}
178 Any value other than `null` or `caddyfile` is only valid when providing
179 your own `configFile`.
180 :::
181 '';
182 };
183
184 resume = mkOption {
185 default = false;
186 type = types.bool;
187 description = lib.mdDoc ''
188 Use saved config, if any (and prefer over any specified configuration passed with `--config`).
189 '';
190 };
191
192 globalConfig = mkOption {
193 type = types.lines;
194 default = "";
195 example = ''
196 debug
197 servers {
198 protocol {
199 experimental_http3
200 }
201 }
202 '';
203 description = lib.mdDoc ''
204 Additional lines of configuration appended to the global config section
205 of the `Caddyfile`.
206
207 Refer to <https://caddyserver.com/docs/caddyfile/options#global-options>
208 for details on supported values.
209 '';
210 };
211
212 extraConfig = mkOption {
213 type = types.lines;
214 default = "";
215 example = ''
216 example.com {
217 encode gzip
218 log
219 root /srv/http
220 }
221 '';
222 description = lib.mdDoc ''
223 Additional lines of configuration appended to the automatically
224 generated `Caddyfile`.
225 '';
226 };
227
228 virtualHosts = mkOption {
229 type = with types; attrsOf (submodule (import ./vhost-options.nix { inherit cfg; }));
230 default = {};
231 example = literalExpression ''
232 {
233 "hydra.example.com" = {
234 serverAliases = [ "www.hydra.example.com" ];
235 extraConfig = '''
236 encode gzip
237 root /srv/http
238 ''';
239 };
240 };
241 '';
242 description = lib.mdDoc ''
243 Declarative specification of virtual hosts served by Caddy.
244 '';
245 };
246
247 acmeCA = mkOption {
248 default = "https://acme-v02.api.letsencrypt.org/directory";
249 example = "https://acme-staging-v02.api.letsencrypt.org/directory";
250 type = with types; nullOr str;
251 description = lib.mdDoc ''
252 The URL to the ACME CA's directory. It is strongly recommended to set
253 this to Let's Encrypt's staging endpoint for testing or development.
254
255 Set it to `null` if you want to write a more
256 fine-grained configuration manually.
257 '';
258 };
259
260 email = mkOption {
261 default = null;
262 type = with types; nullOr str;
263 description = lib.mdDoc ''
264 Your email address. Mainly used when creating an ACME account with your
265 CA, and is highly recommended in case there are problems with your
266 certificates.
267 '';
268 };
269
270 };
271
272 # implementation
273 config = mkIf cfg.enable {
274
275 assertions = [
276 { assertion = cfg.configFile == configFile -> cfg.adapter == "caddyfile" || cfg.adapter == null;
277 message = "To specify an adapter other than 'caddyfile' please provide your own configuration via `services.caddy.configFile`";
278 }
279 ] ++ map (name: mkCertOwnershipAssertion {
280 inherit (cfg) group user;
281 cert = config.security.acme.certs.${name};
282 groups = config.users.groups;
283 }) acmeHosts;
284
285 services.caddy.extraConfig = concatMapStringsSep "\n" mkVHostConf virtualHosts;
286 services.caddy.globalConfig = ''
287 ${optionalString (cfg.email != null) "email ${cfg.email}"}
288 ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"}
289 log {
290 ${cfg.logFormat}
291 }
292 '';
293
294 # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
295 boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
296
297 systemd.packages = [ cfg.package ];
298 systemd.services.caddy = {
299 wants = map (hostOpts: "acme-finished-${hostOpts.useACMEHost}.target") acmeVHosts;
300 after = map (hostOpts: "acme-selfsigned-${hostOpts.useACMEHost}.service") acmeVHosts;
301 before = map (hostOpts: "acme-${hostOpts.useACMEHost}.service") acmeVHosts;
302
303 wantedBy = [ "multi-user.target" ];
304 startLimitIntervalSec = 14400;
305 startLimitBurst = 10;
306
307 serviceConfig = {
308 # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart=
309 # 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.
310 ExecStart = [ "" ''${cfg.package}/bin/caddy run --config ${cfg.configFile} ${optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"} ${optionalString cfg.resume "--resume"}'' ];
311 ExecReload = [ "" ''${cfg.package}/bin/caddy reload --config ${cfg.configFile} ${optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"} --force'' ];
312 ExecStartPre = ''${cfg.package}/bin/caddy validate --config ${cfg.configFile} ${optionalString (cfg.adapter != null) "--adapter ${cfg.adapter}"}'';
313 User = cfg.user;
314 Group = cfg.group;
315 ReadWriteDirectories = cfg.dataDir;
316 StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ];
317 LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ];
318 Restart = "on-abnormal";
319
320 # TODO: attempt to upstream these options
321 NoNewPrivileges = true;
322 PrivateDevices = true;
323 ProtectHome = true;
324 };
325 };
326
327 users.users = optionalAttrs (cfg.user == "caddy") {
328 caddy = {
329 group = cfg.group;
330 uid = config.ids.uids.caddy;
331 home = cfg.dataDir;
332 };
333 };
334
335 users.groups = optionalAttrs (cfg.group == "caddy") {
336 caddy.gid = config.ids.gids.caddy;
337 };
338
339 security.acme.certs =
340 let
341 certCfg = map (useACMEHost: nameValuePair useACMEHost {
342 group = mkDefault cfg.group;
343 reloadServices = [ "caddy.service" ];
344 }) acmeHosts;
345 in
346 listToAttrs certCfg;
347
348 };
349}