1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.caddy;
7 configFile = pkgs.writeText "Caddyfile" cfg.config;
8
9 tlsConfig = {
10 apps.tls.automation.policies = [{
11 issuer = {
12 inherit (cfg) ca email;
13 module = "acme";
14 };
15 }];
16 };
17
18 adaptedConfig = pkgs.runCommand "caddy-config-adapted.json" { } ''
19 ${cfg.package}/bin/caddy adapt \
20 --config ${configFile} --adapter ${cfg.adapter} > $out
21 '';
22 tlsJSON = pkgs.writeText "tls.json" (builtins.toJSON tlsConfig);
23
24 # merge the TLS config options we expose with the ones originating in the Caddyfile
25 configJSON =
26 let tlsConfigMerge = ''
27 {"apps":
28 {"tls":
29 {"automation":
30 {"policies":
31 (if .[0].apps.tls.automation.policies == .[1]?.apps.tls.automation.policies
32 then .[0].apps.tls.automation.policies
33 else (.[0].apps.tls.automation.policies + .[1]?.apps.tls.automation.policies)
34 end)
35 }
36 }
37 }
38 }'';
39 in pkgs.runCommand "caddy-config.json" { } ''
40 ${pkgs.jq}/bin/jq -s '.[0] * ${tlsConfigMerge}' ${adaptedConfig} ${tlsJSON} > $out
41 '';
42in {
43 imports = [
44 (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2")
45 ];
46
47 options.services.caddy = {
48 enable = mkEnableOption "Caddy web server";
49
50 config = mkOption {
51 default = "";
52 example = ''
53 example.com {
54 encode gzip
55 log
56 root /srv/http
57 }
58 '';
59 type = types.lines;
60 description = ''
61 Verbatim Caddyfile to use.
62 Caddy v2 supports multiple config formats via adapters (see <option>services.caddy.adapter</option>).
63 '';
64 };
65
66 user = mkOption {
67 default = "caddy";
68 type = types.str;
69 description = "User account under which caddy runs.";
70 };
71
72 group = mkOption {
73 default = "caddy";
74 type = types.str;
75 description = "Group account under which caddy runs.";
76 };
77
78 adapter = mkOption {
79 default = "caddyfile";
80 example = "nginx";
81 type = types.str;
82 description = ''
83 Name of the config adapter to use.
84 See https://caddyserver.com/docs/config-adapters for the full list.
85 '';
86 };
87
88 ca = mkOption {
89 default = "https://acme-v02.api.letsencrypt.org/directory";
90 example = "https://acme-staging-v02.api.letsencrypt.org/directory";
91 type = types.str;
92 description = "Certificate authority ACME server. The default (Let's Encrypt production server) should be fine for most people.";
93 };
94
95 email = mkOption {
96 default = "";
97 type = types.str;
98 description = "Email address (for Let's Encrypt certificate)";
99 };
100
101 dataDir = mkOption {
102 default = "/var/lib/caddy";
103 type = types.path;
104 description = ''
105 The data directory, for storing certificates. Before 17.09, this
106 would create a .caddy directory. With 17.09 the contents of the
107 .caddy directory are in the specified data directory instead.
108
109 Caddy v2 replaced CADDYPATH with XDG directories.
110 See https://caddyserver.com/docs/conventions#file-locations.
111 '';
112 };
113
114 package = mkOption {
115 default = pkgs.caddy;
116 defaultText = "pkgs.caddy";
117 example = "pkgs.caddy";
118 type = types.package;
119 description = ''
120 Caddy package to use.
121 '';
122 };
123 };
124
125 config = mkIf cfg.enable {
126 systemd.services.caddy = {
127 description = "Caddy web server";
128 # upstream unit: https://github.com/caddyserver/dist/blob/master/init/caddy.service
129 after = [ "network-online.target" ];
130 wants = [ "network-online.target" ]; # systemd-networkd-wait-online.service
131 wantedBy = [ "multi-user.target" ];
132 startLimitIntervalSec = 14400;
133 startLimitBurst = 10;
134 serviceConfig = {
135 ExecStart = "${cfg.package}/bin/caddy run --config ${configJSON}";
136 ExecReload = "${cfg.package}/bin/caddy reload --config ${configJSON}";
137 Type = "simple";
138 User = cfg.user;
139 Group = cfg.group;
140 Restart = "on-abnormal";
141 AmbientCapabilities = "cap_net_bind_service";
142 CapabilityBoundingSet = "cap_net_bind_service";
143 NoNewPrivileges = true;
144 LimitNPROC = 512;
145 LimitNOFILE = 1048576;
146 PrivateTmp = true;
147 PrivateDevices = true;
148 ProtectHome = true;
149 ProtectSystem = "full";
150 ReadWriteDirectories = cfg.dataDir;
151 KillMode = "mixed";
152 KillSignal = "SIGQUIT";
153 TimeoutStopSec = "5s";
154 };
155 };
156
157 users.users = optionalAttrs (cfg.user == "caddy") {
158 caddy = {
159 group = cfg.group;
160 uid = config.ids.uids.caddy;
161 home = cfg.dataDir;
162 createHome = true;
163 };
164 };
165
166 users.groups = optionalAttrs (cfg.group == "caddy") {
167 caddy.gid = config.ids.gids.caddy;
168 };
169
170 };
171}