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