1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.security.acme;
8
9 certOpts = { ... }: {
10 options = {
11 webroot = mkOption {
12 type = types.str;
13 description = ''
14 Where the webroot of the HTTP vhost is located.
15 <filename>.well-known/acme-challenge/</filename> directory
16 will be created automatically if it doesn't exist.
17 <literal>http://example.org/.well-known/acme-challenge/</literal> must also
18 be available (notice unencrypted HTTP).
19 '';
20 };
21
22 email = mkOption {
23 type = types.nullOr types.str;
24 default = null;
25 description = "Contact email address for the CA to be able to reach you.";
26 };
27
28 user = mkOption {
29 type = types.str;
30 default = "root";
31 description = "User running the ACME client.";
32 };
33
34 group = mkOption {
35 type = types.str;
36 default = "root";
37 description = "Group running the ACME client.";
38 };
39
40 allowKeysForGroup = mkOption {
41 type = types.bool;
42 default = false;
43 description = "Give read permissions to the specified group to read SSL private certificates.";
44 };
45
46 postRun = mkOption {
47 type = types.lines;
48 default = "";
49 example = "systemctl reload nginx.service";
50 description = ''
51 Commands to run after certificates are re-issued. Typically
52 the web server and other servers using certificates need to
53 be reloaded.
54 '';
55 };
56
57 plugins = mkOption {
58 type = types.listOf (types.enum [
59 "cert.der" "cert.pem" "chain.pem" "external.sh"
60 "fullchain.pem" "full.pem" "key.der" "key.pem" "account_key.json"
61 ]);
62 default = [ "fullchain.pem" "key.pem" "account_key.json" ];
63 description = ''
64 Plugins to enable. With default settings simp_le will
65 store public certificate bundle in <filename>fullchain.pem</filename>
66 and private key in <filename>key.pem</filename> in its state directory.
67 '';
68 };
69
70 extraDomains = mkOption {
71 type = types.attrsOf (types.nullOr types.str);
72 default = {};
73 example = {
74 "example.org" = "/srv/http/nginx";
75 "mydomain.org" = null;
76 };
77 description = ''
78 Extra domain names for which certificates are to be issued, with their
79 own server roots if needed.
80 '';
81 };
82 };
83 };
84
85in
86
87{
88
89 ###### interface
90
91 options = {
92 security.acme = {
93 directory = mkOption {
94 default = "/var/lib/acme";
95 type = types.str;
96 description = ''
97 Directory where certs and other state will be stored by default.
98 '';
99 };
100
101 validMin = mkOption {
102 type = types.int;
103 default = 30 * 24 * 3600;
104 description = "Minimum remaining validity before renewal in seconds.";
105 };
106
107 renewInterval = mkOption {
108 type = types.str;
109 default = "weekly";
110 description = ''
111 Systemd calendar expression when to check for renewal. See
112 <citerefentry><refentrytitle>systemd.time</refentrytitle>
113 <manvolnum>5</manvolnum></citerefentry>.
114 '';
115 };
116
117 preliminarySelfsigned = mkOption {
118 type = types.bool;
119 default = true;
120 description = ''
121 Whether a preliminary self-signed certificate should be generated before
122 doing ACME requests. This can be useful when certificates are required in
123 a webserver, but ACME needs the webserver to make its requests.
124
125 With preliminary self-signed certificate the webserver can be started and
126 can later reload the correct ACME certificates.
127 '';
128 };
129
130 certs = mkOption {
131 default = { };
132 type = types.loaOf types.optionSet;
133 description = ''
134 Attribute set of certificates to get signed and renewed.
135 '';
136 options = [ certOpts ];
137 example = {
138 "example.com" = {
139 webroot = "/var/www/challenges/";
140 email = "foo@example.com";
141 extraDomains = { "www.example.com" = null; "foo.example.com" = "/var/www/foo/"; };
142 };
143 "bar.example.com" = {
144 webroot = "/var/www/challenges/";
145 email = "bar@example.com";
146 };
147 };
148 };
149 };
150 };
151
152 ###### implementation
153 config = mkMerge [
154 (mkIf (cfg.certs != { }) {
155
156 systemd.services = let
157 services = concatLists servicesLists;
158 servicesLists = mapAttrsToList certToServices cfg.certs;
159 certToServices = cert: data:
160 let
161 cpath = "${cfg.directory}/${cert}";
162 rights = if data.allowKeysForGroup then "750" else "700";
163 cmdline = [ "-v" "-d" cert "--default_root" data.webroot "--valid_min" cfg.validMin ]
164 ++ optionals (data.email != null) [ "--email" data.email ]
165 ++ concatMap (p: [ "-f" p ]) data.plugins
166 ++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains);
167 acmeService = {
168 description = "Renew ACME Certificate for ${cert}";
169 after = [ "network.target" ];
170 serviceConfig = {
171 Type = "oneshot";
172 SuccessExitStatus = [ "0" "1" ];
173 PermissionsStartOnly = true;
174 User = data.user;
175 Group = data.group;
176 PrivateTmp = true;
177 };
178 path = [ pkgs.simp_le ];
179 preStart = ''
180 mkdir -p '${cfg.directory}'
181 if [ ! -d '${cpath}' ]; then
182 mkdir '${cpath}'
183 fi
184 chmod ${rights} '${cpath}'
185 chown -R '${data.user}:${data.group}' '${cpath}'
186 '';
187 script = ''
188 cd '${cpath}'
189 set +e
190 simp_le ${escapeShellArgs cmdline}
191 EXITCODE=$?
192 set -e
193 echo "$EXITCODE" > /tmp/lastExitCode
194 exit "$EXITCODE"
195 '';
196 postStop = ''
197 if [ -e /tmp/lastExitCode ] && [ "$(cat /tmp/lastExitCode)" = "0" ]; then
198 echo "Executing postRun hook..."
199 ${data.postRun}
200 fi
201 '';
202
203 before = [ "acme-certificates.target" ];
204 wantedBy = [ "acme-certificates.target" ];
205 };
206 selfsignedService = {
207 description = "Create preliminary self-signed certificate for ${cert}";
208 preStart = ''
209 if [ ! -d '${cpath}' ]
210 then
211 mkdir -p '${cpath}'
212 chmod ${rights} '${cpath}'
213 chown '${data.user}:${data.group}' '${cpath}'
214 fi
215 '';
216 script =
217 ''
218 # Create self-signed key
219 workdir="/run/acme-selfsigned-${cert}"
220 ${pkgs.openssl.bin}/bin/openssl genrsa -des3 -passout pass:x -out $workdir/server.pass.key 2048
221 ${pkgs.openssl.bin}/bin/openssl rsa -passin pass:x -in $workdir/server.pass.key -out $workdir/server.key
222 ${pkgs.openssl.bin}/bin/openssl req -new -key $workdir/server.key -out $workdir/server.csr \
223 -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com"
224 ${pkgs.openssl.bin}/bin/openssl x509 -req -days 1 -in $workdir/server.csr -signkey $workdir/server.key -out $workdir/server.crt
225
226 # Move key to destination
227 mv $workdir/server.key ${cpath}/key.pem
228 mv $workdir/server.crt ${cpath}/fullchain.pem
229
230 # Clean up working directory
231 rm $workdir/server.csr
232 rm $workdir/server.pass.key
233
234 # Give key acme permissions
235 chmod ${rights} '${cpath}/key.pem'
236 chown '${data.user}:${data.group}' '${cpath}/key.pem'
237 chmod ${rights} '${cpath}/fullchain.pem'
238 chown '${data.user}:${data.group}' '${cpath}/fullchain.pem'
239 '';
240 serviceConfig = {
241 Type = "oneshot";
242 RuntimeDirectory = "acme-selfsigned-${cert}";
243 PermissionsStartOnly = true;
244 User = data.user;
245 Group = data.group;
246 };
247 unitConfig = {
248 # Do not create self-signed key when key already exists
249 ConditionPathExists = "!${cpath}/key.pem";
250 };
251 before = [
252 "acme-selfsigned-certificates.target"
253 ];
254 wantedBy = [
255 "acme-selfsigned-certificates.target"
256 ];
257 };
258 in (
259 [ { name = "acme-${cert}"; value = acmeService; } ]
260 ++
261 (if cfg.preliminarySelfsigned
262 then [ { name = "acme-selfsigned-${cert}"; value = selfsignedService; } ]
263 else []
264 )
265 );
266 servicesAttr = listToAttrs services;
267 nginxAttr = {
268 nginx = {
269 after = [ "acme-selfsigned-certificates.target" ];
270 wants = [ "acme-selfsigned-certificates.target" "acme-certificates.target" ];
271 };
272 };
273 in
274 servicesAttr //
275 (if config.services.nginx.enable then nginxAttr else {});
276
277 systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair
278 ("acme-${cert}")
279 ({
280 description = "Renew ACME Certificate for ${cert}";
281 wantedBy = [ "timers.target" ];
282 timerConfig = {
283 OnCalendar = cfg.renewInterval;
284 Unit = "acme-${cert}.service";
285 };
286 })
287 );
288
289 systemd.targets."acme-selfsigned-certificates" = mkIf cfg.preliminarySelfsigned {};
290 systemd.targets."acme-certificates" = {};
291 })
292
293 ];
294
295 meta = {
296 maintainers = with lib.maintainers; [ abbradar fpletz globin ];
297 doc = ./acme.xml;
298 };
299}