1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 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 };
27
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
53 ''
54 else
55 adaptedConfig;
56in
57{
58 imports = [
59 (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2")
60 ];
61
62 options.services.caddy = {
63 enable = mkEnableOption "Caddy web server";
64
65 config = mkOption {
66 default = "";
67 example = ''
68 example.com {
69 encode gzip
70 log
71 root /srv/http
72 }
73 '';
74 type = types.lines;
75 description = ''
76 Verbatim Caddyfile to use.
77 Caddy v2 supports multiple config formats via adapters (see <option>services.caddy.adapter</option>).
78 '';
79 };
80
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 };
97 '';
98 description = "Declarative vhost config";
99 };
100
101
102 user = mkOption {
103 default = "caddy";
104 type = types.str;
105 description = "User account under which caddy runs.";
106 };
107
108 group = mkOption {
109 default = "caddy";
110 type = types.str;
111 description = "Group account under which caddy runs.";
112 };
113
114 adapter = mkOption {
115 default = "caddyfile";
116 example = "nginx";
117 type = types.str;
118 description = ''
119 Name of the config adapter to use.
120 See https://caddyserver.com/docs/config-adapters for the full list.
121 '';
122 };
123
124 resume = mkOption {
125 default = false;
126 type = types.bool;
127 description = ''
128 Use saved config, if any (and prefer over configuration passed with <option>services.caddy.config</option>).
129 '';
130 };
131
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;
136 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)
141 '';
142 };
143
144 email = mkOption {
145 default = "";
146 type = types.str;
147 description = "Email address (for Let's Encrypt certificate)";
148 };
149
150 dataDir = mkOption {
151 default = "/var/lib/caddy";
152 type = types.path;
153 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.
157
158 Caddy v2 replaced CADDYPATH with XDG directories.
159 See https://caddyserver.com/docs/conventions#file-locations.
160 '';
161 };
162
163 package = mkOption {
164 default = pkgs.caddy;
165 defaultText = literalExpression "pkgs.caddy";
166 type = types.package;
167 description = ''
168 Caddy package to use.
169 '';
170 };
171 };
172
173 config = mkIf cfg.enable {
174 systemd.services.caddy = {
175 description = "Caddy web server";
176 # upstream unit: https://github.com/caddyserver/dist/blob/master/init/caddy.service
177 after = [ "network-online.target" ];
178 wants = [ "network-online.target" ]; # systemd-networkd-wait-online.service
179 wantedBy = [ "multi-user.target" ];
180 startLimitIntervalSec = 14400;
181 startLimitBurst = 10;
182 serviceConfig = {
183 ExecStart = "${cfg.package}/bin/caddy run ${optionalString cfg.resume "--resume"} --config ${configJSON}";
184 ExecReload = "${cfg.package}/bin/caddy reload --config ${configJSON}";
185 Type = "simple";
186 User = cfg.user;
187 Group = cfg.group;
188 Restart = "on-abnormal";
189 AmbientCapabilities = "cap_net_bind_service";
190 CapabilityBoundingSet = "cap_net_bind_service";
191 NoNewPrivileges = true;
192 LimitNPROC = 512;
193 LimitNOFILE = 1048576;
194 PrivateTmp = true;
195 PrivateDevices = true;
196 ProtectHome = true;
197 ProtectSystem = "full";
198 ReadWriteDirectories = cfg.dataDir;
199 KillMode = "mixed";
200 KillSignal = "SIGQUIT";
201 TimeoutStopSec = "5s";
202 };
203 };
204
205 users.users = optionalAttrs (cfg.user == "caddy") {
206 caddy = {
207 group = cfg.group;
208 uid = config.ids.uids.caddy;
209 home = cfg.dataDir;
210 createHome = true;
211 };
212 };
213
214 users.groups = optionalAttrs (cfg.group == "caddy") {
215 caddy.gid = config.ids.gids.caddy;
216 };
217
218 };
219}