git.iko.soy/typeable/nixpkgs
on master help sign in
nixos/caddy: introduce several new options
commit81a67a3353b09c0abade5f2d17e91d23873fc7fb authorAaron Andersen <aaron@fosslib.net> dateMon 29 Nov 2021, 22:39 (5 years ago) parentcf78894
2 files changed+271−123
nixos/modules/services/web-servers/caddy/default.nix+210−113 nixos/modules/services/web-servers/caddy/vhost-options.nix+61−10
nixos/modules/services/web-servers/caddy/default.nix+210−113
@@ -4,111 +4,159 @@ with lib;
44
55let
66 cfg = config.services.caddy;
7− vhostToConfig = vhostName: vhostAttrs: ''
8− ${vhostName} ${builtins.concatStringsSep " " vhostAttrs.serverAliases} {
9− ${vhostAttrs.extraConfig}
10− }
11− '';
12− configFile = pkgs.writeText "Caddyfile" (builtins.concatStringsSep "\n"
13− ([ cfg.config ] ++ (mapAttrsToList vhostToConfig cfg.virtualHosts)));
14−
15− formattedConfig = pkgs.runCommand "formattedCaddyFile" { } ''
16− ${cfg.package}/bin/caddy fmt ${configFile} > $out
17− '';
18−
19− tlsConfig = {
20− apps.tls.automation.policies = [{
21− issuers = [{
22− inherit (cfg) ca email;
23− module = "acme";
24− }];
25− }];
26− };
277
28− adaptedConfig = pkgs.runCommand "caddy-config-adapted.json" { } ''
29− ${cfg.package}/bin/caddy adapt \
30− --config ${formattedConfig} --adapter ${cfg.adapter} > $out
31− '';
32− tlsJSON = pkgs.writeText "tls.json" (builtins.toJSON tlsConfig);
33−
34− # merge the TLS config options we expose with the ones originating in the Caddyfile
35− configJSON =
36− if cfg.ca != null then
37− let tlsConfigMerge = ''
38− {"apps":
39− {"tls":
40− {"automation":
41− {"policies":
42− (if .[0].apps.tls.automation.policies == .[1]?.apps.tls.automation.policies
43− then .[0].apps.tls.automation.policies
44− else (.[0].apps.tls.automation.policies + .[1]?.apps.tls.automation.policies)
45− end)
46− }
47− }
48− }
49− }'';
50− in
51− pkgs.runCommand "caddy-config.json" { } ''
52− ${pkgs.jq}/bin/jq -s '.[0] * ${tlsConfigMerge}' ${adaptedConfig} ${tlsJSON} > $out
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
5315 ''
54− else
55− adaptedConfig;
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.writeText "Caddyfile" ''
30+ {
31+ ${optionalString (cfg.email != null) "email ${cfg.email}"}
32+ ${optionalString (cfg.acmeCA != null) "acme_ca ${cfg.acmeCA}"}
33+ log {
34+ ${cfg.logFormat}
35+ }
36+ }
37+ ${cfg.extraConfig}
38+ '';
39+
40+ Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } ''
41+ ${cfg.package}/bin/caddy fmt ${Caddyfile} > $out
42+ '';
43+ in
44+ if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile;
5645in
5746{
5847 imports = [
5948 (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2")
49+ (mkRenamedOptionModule [ "services" "caddy" "ca" ] [ "services" "caddy" "acmeCA" ])
50+ (mkRenamedOptionModule [ "services" "caddy" "config" ] [ "services" "caddy" "extraConfig" ])
6051 ];
6152
53+ # interface
6254 options.services.caddy = {
6355 enable = mkEnableOption "Caddy web server";
6456
65− config = mkOption {
66− default = "";
67− example = ''
68− example.com {
69− encode gzip
70− log
71− root /srv/http
72− }
57+ user = mkOption {
58+ default = "caddy";
59+ type = types.str;
60+ description = ''
61+ User account under which caddy runs.
62+
63+ <note><para>
64+ If left as the default value this user will automatically be created
65+ on system activation, otherwise you are responsible for
66+ ensuring the user exists before the Caddy service starts.
67+ </para></note>
7368 '';
74− type = types.lines;
69+ };
70+
71+ group = mkOption {
72+ default = "caddy";
73+ type = types.str;
7574 description = ''
76− Verbatim Caddyfile to use.
77− Caddy v2 supports multiple config formats via adapters (see <option>services.caddy.adapter</option>).
75+ Group account under which caddy runs.
76+
77+ <note><para>
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+ </para></note>
7882 '';
7983 };
8084
81− virtualHosts = mkOption {
82− type = types.attrsOf (types.submodule (import ./vhost-options.nix {
83− inherit config lib;
84− }));
85− default = { };
86− example = literalExpression ''
87− {
88− "hydra.example.com" = {
89− serverAliases = [ "www.hydra.example.com" ];
90− extraConfig = ''''''
91− encode gzip
92− log
93− root /srv/http
94− '''''';
95− };
96− };
85+ package = mkOption {
86+ default = pkgs.caddy;
87+ defaultText = literalExpression "pkgs.caddy";
88+ type = types.package;
89+ description = ''
90+ Caddy package to use.
9791 '';
98− description = "Declarative vhost config";
9992 };
10093
94+ dataDir = mkOption {
95+ type = types.path;
96+ default = "/var/lib/caddy";
97+ description = ''
98+ The data directory for caddy.
10199
102− user = mkOption {
103− default = "caddy";
104− type = types.str;
105− description = "User account under which caddy runs.";
100+ <note>
101+ <para>
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+ </para>
106+ <para>
107+ Caddy v2 replaced <literal>CADDYPATH</literal> with XDG directories.
108+ See <link xlink:href="https://caddyserver.com/docs/conventions#file-locations"/>.
109+ </para>
110+ </note>
111+ '';
106112 };
107113
108− group = mkOption {
109− default = "caddy";
110− type = types.str;
111− description = "Group account under which caddy runs.";
114+ logDir = mkOption {
115+ type = types.path;
116+ default = "/var/log/caddy";
117+ description = ''
118+ Directory for storing Caddy access logs.
119+
120+ <note><para>
121+ If left as the default value this directory will automatically be created
122+ before the Caddy server starts, otherwise the sysadmin is responsible for
123+ ensuring the directory exists with appropriate ownership and permissions.
124+ </para></note>
125+ '';
126+ };
127+
128+ logFormat = mkOption {
129+ type = types.lines;
130+ default = ''
131+ level ERROR
132+ '';
133+ example = literalExpression ''
134+ mkForce "level INFO";
135+ '';
136+ description = ''
137+ Configuration for the default logger. See
138+ <link xlink:href="https://caddyserver.com/docs/caddyfile/options#log"/>
139+ for details.
140+ '';
141+ };
142+
143+ configFile = mkOption {
144+ type = types.path;
145+ default = configFile;
146+ defaultText = "A Caddyfile automatically generated by values from services.caddy.*";
147+ example = literalExpression ''
148+ pkgs.writeText "Caddyfile" '''
149+ example.com
150+
151+ root * /var/www/wordpress
152+ php_fastcgi unix//run/php/php-version-fpm.sock
153+ file_server
154+ ''';
155+ '';
156+ description = ''
157+ Override the configuration file used by Caddy. By default,
158+ NixOS generates one automatically.
159+ '';
112160 };
113161
114162 adapter = mkOption {
@@ -117,7 +165,13 @@ in
117165 type = types.str;
118166 description = ''
119167 Name of the config adapter to use.
120− See https://caddyserver.com/docs/config-adapters for the full list.
168+ See <link xlink:href="https://caddyserver.com/docs/config-adapters"/>
169+ for the full list.
170+
171+ <note><para>
172+ Any value other than <literal>caddyfile</literal> is only valid when
173+ providing your own <option>configFile</option>.
174+ </para></note>
121175 '';
122176 };
123177
@@ -125,54 +179,87 @@ in
125179 default = false;
126180 type = types.bool;
127181 description = ''
128− Use saved config, if any (and prefer over configuration passed with <option>services.caddy.config</option>).
182+ Use saved config, if any (and prefer over any specified configuration passed with <literal>--config</literal>).
129183 '';
130184 };
131185
132− ca = mkOption {
133− default = "https://acme-v02.api.letsencrypt.org/directory";
134− example = "https://acme-staging-v02.api.letsencrypt.org/directory";
135− type = types.nullOr types.str;
186+ extraConfig = mkOption {
187+ type = types.lines;
188+ default = "";
189+ example = ''
190+ example.com {
191+ encode gzip
192+ log
193+ root /srv/http
194+ }
195+ '';
136196 description = ''
137− Certificate authority ACME server. The default (Let's Encrypt
138− production server) should be fine for most people. Set it to null if
139− you don't want to include any authority (or if you want to write a more
140− fine-graned configuration manually)
197+ Additional lines of configuration appended to the automatically
198+ generated <literal>Caddyfile</literal>.
141199 '';
142200 };
143201
144− email = mkOption {
145− default = "";
146− type = types.str;
147− description = "Email address (for Let's Encrypt certificate)";
202+ virtualHosts = mkOption {
203+ type = with types; attrsOf (submodule (import ./vhost-options.nix { inherit cfg; }));
204+ default = {};
205+ example = literalExpression ''
206+ {
207+ "hydra.example.com" = {
208+ serverAliases = [ "www.hydra.example.com" ];
209+ extraConfig = '''
210+ encode gzip
211+ root /srv/http
212+ ''';
213+ };
214+ };
215+ '';
216+ description = ''
217+ Declarative specification of virtual hosts served by Caddy.
218+ '';
148219 };
149220
150− dataDir = mkOption {
151− default = "/var/lib/caddy";
152− type = types.path;
221+ acmeCA = mkOption {
222+ default = "https://acme-v02.api.letsencrypt.org/directory";
223+ example = "https://acme-staging-v02.api.letsencrypt.org/directory";
224+ type = with types; nullOr str;
153225 description = ''
154− The data directory, for storing certificates. Before 17.09, this
155− would create a .caddy directory. With 17.09 the contents of the
156− .caddy directory are in the specified data directory instead.
226+ The URL to the ACME CA's directory. It is strongly recommended to set
227+ this to Let's Encrypt's staging endpoint for testing or development.
157228
158− Caddy v2 replaced CADDYPATH with XDG directories.
159− See https://caddyserver.com/docs/conventions#file-locations.
229+ Set it to <literal>null</literal> if you want to write a more
230+ fine-grained configuration manually.
160231 '';
161232 };
162233
163− package = mkOption {
164− default = pkgs.caddy;
165− defaultText = literalExpression "pkgs.caddy";
166− type = types.package;
234+ email = mkOption {
235+ default = null;
236+ type = with types; nullOr str;
167237 description = ''
168− Caddy package to use.
238+ Your email address. Mainly used when creating an ACME account with your
239+ CA, and is highly recommended in case there are problems with your
240+ certificates.
169241 '';
170242 };
243+
171244 };
172245
246+ # implementation
173247 config = mkIf cfg.enable {
248+
249+ assertions = [
250+ { assertion = cfg.adapter != "caddyfile" -> cfg.configFile != configFile;
251+ message = "Any value other than 'caddyfile' is only valid when providing your own `services.caddy.configFile`";
252+ }
253+ ];
254+
255+ services.caddy.extraConfig = concatMapStringsSep "\n" mkVHostConf virtualHosts;
256+
174257 systemd.packages = [ cfg.package ];
175258 systemd.services.caddy = {
259+ wants = map (hostOpts: "acme-finished-${hostOpts.useACMEHost}.target") acmeVHosts;
260+ after = map (hostOpts: "acme-selfsigned-${hostOpts.useACMEHost}.service") acmeVHosts;
261+ before = map (hostOpts: "acme-${hostOpts.useACMEHost}.service") acmeVHosts;
262+
176263 wantedBy = [ "multi-user.target" ];
177264 startLimitIntervalSec = 14400;
178265 startLimitBurst = 10;
@@ -180,13 +267,17 @@ in
180267 serviceConfig = {
181268 # https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart=
182269 # 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.
183− ExecStart = [ "" "${cfg.package}/bin/caddy run ${optionalString cfg.resume "--resume"} --config ${configJSON}" ];
184− ExecReload = [ "" "${cfg.package}/bin/caddy reload --config ${configJSON}" ];
270+ ExecStart = [ "" "${cfg.package}/bin/caddy run --config ${cfg.configFile} --adapter ${cfg.adapter} ${optionalString cfg.resume "--resume"}" ];
271+ ExecReload = [ "" "${cfg.package}/bin/caddy reload --config ${cfg.configFile} --adapter ${cfg.adapter}" ];
185272
273+ ExecStartPre = "${cfg.package}/bin/caddy validate --config ${cfg.configFile} --adapter ${cfg.adapter}";
186274 User = cfg.user;
187275 Group = cfg.group;
188276 ReadWriteDirectories = cfg.dataDir;
277+ StateDirectory = mkIf (cfg.dataDir == "/var/lib/caddy") [ "caddy" ];
278+ LogsDirectory = mkIf (cfg.logDir == "/var/log/caddy") [ "caddy" ];
189279 Restart = "on-abnormal";
280+ SupplementaryGroups = mkIf (length acmeVHosts != 0) [ "acme" ];
190281
191282 # TODO: attempt to upstream these options
192283 NoNewPrivileges = true;
@@ -200,7 +291,6 @@ in
200291 group = cfg.group;
201292 uid = config.ids.uids.caddy;
202293 home = cfg.dataDir;
203− createHome = true;
204294 };
205295 };
206296
@@ -208,5 +298,12 @@ in
208298 caddy.gid = config.ids.gids.caddy;
209299 };
210300
301+ security.acme.certs =
302+ let
303+ eachACMEHost = unique (catAttrs "useACMEHost" acmeVHosts);
304+ reloads = map (useACMEHost: nameValuePair useACMEHost { reloadServices = [ "caddy.service" ]; }) eachACMEHost;
305+ in
306+ listToAttrs reloads;
307+
211308 };
212309}
nixos/modules/services/web-servers/caddy/vhost-options.nix+61−10
@@ -1,15 +1,19 @@
1−# This file defines the options that can be used both for the Nginx
2−# main server configuration, and for the virtual hosts. (The latter
3−# has additional options that affect the web server as a whole, like
4−# the user/group to run under.)
5−
6−{ lib, ... }:
7−
8−with lib;
1+{ cfg }:
2+{ config, lib, name, ... }:
3+let
4+ inherit (lib) literalExpression mkOption types;
5+in
96{
107 options = {
8+
9+ hostName = mkOption {
10+ type = types.str;
11+ default = name;
12+ description = "Canonical hostname for the server.";
13+ };
14+
1115 serverAliases = mkOption {
12− type = types.listOf types.str;
16+ type = with types; listOf str;
1317 default = [ ];
1418 example = [ "www.example.org" "example.org" ];
1519 description = ''
@@ -17,12 +21,59 @@ with lib;
1721 '';
1822 };
1923
24+ listenAddresses = mkOption {
25+ type = with types; listOf str;
26+ description = ''
27+ A list of host interfaces to bind to for this virtual host.
28+ '';
29+ default = [ ];
30+ example = [ "127.0.0.1" "::1" ];
31+ };
32+
33+ useACMEHost = mkOption {
34+ type = types.nullOr types.str;
35+ default = null;
36+ description = ''
37+ A host of an existing Let's Encrypt certificate to use.
38+ This is mostly useful if you use DNS challenges but Caddy does not
39+ currently support your provider.
40+
41+ <emphasis>Note that this option does not create any certificates, nor
42+ does it add subdomains to existing ones – you will need to create them
43+ manually using <xref linkend="opt-security.acme.certs"/>. Additionally,
44+ you should probably add the <literal>caddy</literal> user to the
45+ <literal>acme</literal> group to grant access to the certificates.</emphasis>
46+ '';
47+ };
48+
49+ logFormat = mkOption {
50+ type = types.lines;
51+ default = ''
52+ output file ${cfg.logDir}/access-${config.hostName}.log
53+ '';
54+ defaultText = ''
55+ output file ''${config.services.caddy.logDir}/access-''${hostName}.log
56+ '';
57+ example = literalExpression ''
58+ mkForce '''
59+ output discard
60+ ''';
61+ '';
62+ description = ''
63+ Configuration for HTTP request logging (also known as access logs). See
64+ <link xlink:href="https://caddyserver.com/docs/caddyfile/directives/log#log"/>
65+ for details.
66+ '';
67+ };
68+
2069 extraConfig = mkOption {
2170 type = types.lines;
2271 default = "";
2372 description = ''
24− These lines go into the vhost verbatim
73+ Additional lines of configuration appended to this virtual host in the
74+ automatically generated <literal>Caddyfile</literal>.
2575 '';
2676 };
77+
2778 };
2879}