on ls-expose-routeLink-events help sign in
obelisk/acme.nix
397 lines 15 kB history raw
1### COPIED FROM
2### https://github.com/NixOS/nixpkgs/blob/1d36ad6d16dbf1d3937f899a087a4360332eb141/nixos/modules/security/acme.nix
3###
4### to support new plugins in simp_le for ACMEv2 support.
5### Changed lines are marked with a comment: ### CHANGED
7{ config, lib, pkgs, ... }:
9with lib;
11let
13 cfg = config.security.acme;
15 certOpts = { name, ... }: {
16 options = {
17 webroot = mkOption {
18 type = types.str;
19 example = "/var/lib/acme/acme-challenges";
20 description = ''
21 Where the webroot of the HTTP vhost is located.
22 <filename>.well-known/acme-challenge/</filename> directory
23 will be created below the webroot if it doesn't exist.
24 <literal>http://example.org/.well-known/acme-challenge/</literal> must also
25 be available (notice unencrypted HTTP).
26 '';
27 };
29 domain = mkOption {
30 type = types.str;
31 default = name;
32 description = "Domain to fetch certificate for (defaults to the entry name)";
33 };
35 email = mkOption {
36 type = types.nullOr types.str;
37 default = null;
38 description = "Contact email address for the CA to be able to reach you.";
39 };
41 user = mkOption {
42 type = types.str;
43 default = "root";
44 description = "User running the ACME client.";
45 };
47 group = mkOption {
48 type = types.str;
49 default = "root";
50 description = "Group running the ACME client.";
51 };
53 allowKeysForGroup = mkOption {
54 type = types.bool;
55 default = false;
56 description = ''
57 Give read permissions to the specified group
58 (<option>security.acme.cert.&lt;name&gt;.group</option>) to read SSL private certificates.
59 '';
60 };
62 postRun = mkOption {
63 type = types.lines;
64 default = "";
65 example = "systemctl reload nginx.service";
66 description = ''
67 Commands to run after new certificates go live. Typically
68 the web server and other servers using certificates need to
69 be reloaded.
71 Executed in the same directory with the new certificate.
72 '';
73 };
75 plugins = mkOption {
76 type = types.listOf (types.enum [
77 "cert.der" "cert.pem" "chain.pem" "external.sh"
78 "fullchain.pem" "full.pem" "key.der" "key.pem" "account_key.json" "account_reg.json" ### CHANGED
79 ]);
80 default = [ "fullchain.pem" "full.pem" "key.pem" "account_key.json" "account_reg.json" ]; ### CHANGED
81 description = ''
82 Plugins to enable. With default settings simp_le will
83 store public certificate bundle in <filename>fullchain.pem</filename>,
84 private key in <filename>key.pem</filename> and those two previous
85 files combined in <filename>full.pem</filename> in its state directory.
86 '';
87 };
89 activationDelay = mkOption {
90 type = types.nullOr types.str;
91 default = null;
92 description = ''
93 Systemd time span expression to delay copying new certificates to main
94 state directory. See <citerefentry><refentrytitle>systemd.time</refentrytitle>
95 <manvolnum>7</manvolnum></citerefentry>.
96 '';
97 };
99 preDelay = mkOption {
100 type = types.lines;
101 default = "";
102 description = ''
103 Commands to run after certificates are re-issued but before they are
104 activated. Typically the new certificate is published to DNS.
106 Executed in the same directory with the new certificate.
107 '';
108 };
110 extraDomains = mkOption {
111 type = types.attrsOf (types.nullOr types.str);
112 default = {};
113 example = literalExample ''
114 {
115 "example.org" = "/srv/http/nginx";
116 "mydomain.org" = null;
117 }
118 '';
119 description = ''
120 A list of extra domain names, which are included in the one certificate to be issued, with their
121 own server roots if needed.
122 '';
123 };
124 };
125 };
127in
131 ###### interface
133 options = {
134 security.acme = {
135 directory = mkOption {
136 default = "/var/lib/acme";
137 type = types.str;
138 description = ''
139 Directory where certs and other state will be stored by default.
140 '';
141 };
143 validMin = mkOption {
144 type = types.int;
145 default = 30 * 24 * 3600;
146 description = "Minimum remaining validity before renewal in seconds.";
147 };
149 renewInterval = mkOption {
150 type = types.str;
151 default = "weekly";
152 description = ''
153 Systemd calendar expression when to check for renewal. See
154 <citerefentry><refentrytitle>systemd.time</refentrytitle>
155 <manvolnum>7</manvolnum></citerefentry>.
156 '';
157 };
159 preliminarySelfsigned = mkOption {
160 type = types.bool;
161 default = true;
162 description = ''
163 Whether a preliminary self-signed certificate should be generated before
164 doing ACME requests. This can be useful when certificates are required in
165 a webserver, but ACME needs the webserver to make its requests.
167 With preliminary self-signed certificate the webserver can be started and
168 can later reload the correct ACME certificates.
169 '';
170 };
172 production = mkOption {
173 type = types.bool;
174 default = true;
175 description = ''
176 If set to true, use Let's Encrypt's production environment
177 instead of the staging environment. The main benefit of the
178 staging environment is to get much higher rate limits.
180 See
181 <literal>https://letsencrypt.org/docs/staging-environment</literal>
182 for more detail.
183 '';
184 };
186 certs = mkOption {
187 default = { };
188 type = with types; attrsOf (submodule certOpts);
189 description = ''
190 Attribute set of certificates to get signed and renewed.
191 '';
192 example = literalExample ''
193 {
194 "example.com" = {
195 webroot = "/var/www/challenges/";
196 email = "foo@example.com";
197 extraDomains = { "www.example.com" = null; "foo.example.com" = "/var/www/foo/"; };
198 };
199 "bar.example.com" = {
200 webroot = "/var/www/challenges/";
201 email = "bar@example.com";
202 };
203 }
204 '';
205 };
206 };
207 };
209 ###### implementation
210 config = mkMerge [
211 (mkIf (cfg.certs != { }) {
213 systemd.services = let
214 services = concatLists servicesLists;
215 servicesLists = mapAttrsToList certToServices cfg.certs;
216 certToServices = cert: data:
217 let
218 cpath = lpath + optionalString (data.activationDelay != null) ".staging";
219 lpath = "${cfg.directory}/${cert}";
220 rights = if data.allowKeysForGroup then "750" else "700";
221 cmdline = [ "-v" "-d" data.domain "--default_root" data.webroot "--valid_min" cfg.validMin ]
222 ++ optionals (data.email != null) [ "--email" data.email ]
223 ++ concatMap (p: [ "-f" p ]) data.plugins
224 ++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains)
225 ++ optionals (!cfg.production) ["--server" "https://acme-staging.api.letsencrypt.org/directory"];
226 acmeService = {
227 description = "Renew ACME Certificate for ${cert}";
228 after = [ "network.target" "network-online.target" ];
229 wants = [ "network-online.target" ];
230 serviceConfig = {
231 Type = "oneshot";
232 SuccessExitStatus = [ "0" "1" ];
233 PermissionsStartOnly = true;
234 User = data.user;
235 Group = data.group;
236 PrivateTmp = true;
237 };
238 path = with pkgs; [ simp_le systemd ];
239 preStart = ''
240 mkdir -p '${cfg.directory}'
241 chown 'root:root' '${cfg.directory}'
242 chmod 755 '${cfg.directory}'
243 if [ ! -d '${cpath}' ]; then
244 mkdir '${cpath}'
245 fi
246 chmod ${rights} '${cpath}'
247 chown -R '${data.user}:${data.group}' '${cpath}'
248 mkdir -p '${data.webroot}/.well-known/acme-challenge'
249 chown -R '${data.user}:${data.group}' '${data.webroot}/.well-known/acme-challenge'
250 '';
251 script = ''
252 cd '${cpath}'
253 set +e
254 simp_le ${escapeShellArgs cmdline}
255 EXITCODE=$?
256 set -e
257 echo "$EXITCODE" > /tmp/lastExitCode
258 exit "$EXITCODE"
259 '';
260 postStop = ''
261 cd '${cpath}'
263 if [ -e /tmp/lastExitCode ] && [ "$(cat /tmp/lastExitCode)" = "0" ]; then
264 ${if data.activationDelay != null then ''
266 ${data.preDelay}
268 if [ -d '${lpath}' ]; then
269 systemd-run --no-block --on-active='${data.activationDelay}' --unit acme-setlive-${cert}.service
270 else
271 systemctl --wait start acme-setlive-${cert}.service
272 fi
273 '' else data.postRun}
275 # noop ensuring that the "if" block is non-empty even if
276 # activationDelay == null and postRun == ""
277 true
278 fi
279 '';
281 before = [ "acme-certificates.target" ];
282 wantedBy = [ "acme-certificates.target" ];
283 };
284 delayService = {
285 description = "Set certificate for ${cert} live";
286 path = with pkgs; [ rsync ];
287 serviceConfig = {
288 Type = "oneshot";
289 };
290 script = ''
291 rsync -a --delete-after '${cpath}/' '${lpath}'
292 '';
293 postStop = data.postRun;
294 };
295 selfsignedService = {
296 description = "Create preliminary self-signed certificate for ${cert}";
297 path = [ pkgs.openssl ];
298 preStart = ''
299 if [ ! -d '${cpath}' ]
300 then
301 mkdir -p '${cpath}'
302 chmod ${rights} '${cpath}'
303 chown '${data.user}:${data.group}' '${cpath}'
304 fi
305 '';
306 script =
307 ''
308 workdir="$(mktemp -d)"
310 # Create CA
311 openssl genrsa -des3 -passout pass:xxxx -out $workdir/ca.pass.key 2048
312 openssl rsa -passin pass:xxxx -in $workdir/ca.pass.key -out $workdir/ca.key
313 openssl req -new -key $workdir/ca.key -out $workdir/ca.csr \
314 -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=Security Department/CN=example.com"
315 openssl x509 -req -days 1 -in $workdir/ca.csr -signkey $workdir/ca.key -out $workdir/ca.crt
317 # Create key
318 openssl genrsa -des3 -passout pass:xxxx -out $workdir/server.pass.key 2048
319 openssl rsa -passin pass:xxxx -in $workdir/server.pass.key -out $workdir/server.key
320 openssl req -new -key $workdir/server.key -out $workdir/server.csr \
321 -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com"
322 openssl x509 -req -days 1 -in $workdir/server.csr -CA $workdir/ca.crt \
323 -CAkey $workdir/ca.key -CAserial $workdir/ca.srl -CAcreateserial \
324 -out $workdir/server.crt
326 # Copy key to destination
327 cp $workdir/server.key ${cpath}/key.pem
329 # Create fullchain.pem (same format as "simp_le ... -f fullchain.pem" creates)
330 cat $workdir/{server.crt,ca.crt} > "${cpath}/fullchain.pem"
332 # Create full.pem for e.g. lighttpd
333 cat $workdir/{server.key,server.crt,ca.crt} > "${cpath}/full.pem"
335 # Give key acme permissions
336 chown '${data.user}:${data.group}' "${cpath}/"{key,fullchain,full}.pem
337 chmod ${rights} "${cpath}/"{key,fullchain,full}.pem
338 '';
339 serviceConfig = {
340 Type = "oneshot";
341 PermissionsStartOnly = true;
342 PrivateTmp = true;
343 User = data.user;
344 Group = data.group;
345 };
346 unitConfig = {
347 # Do not create self-signed key when key already exists
348 ConditionPathExists = "!${cpath}/key.pem";
349 };
350 before = [
351 "acme-selfsigned-certificates.target"
352 ];
353 wantedBy = [
354 "acme-selfsigned-certificates.target"
355 ];
356 };
357 in (
358 [ { name = "acme-${cert}"; value = acmeService; } ]
359 ++ optional cfg.preliminarySelfsigned { name = "acme-selfsigned-${cert}"; value = selfsignedService; }
360 ++ optional (data.activationDelay != null) { name = "acme-setlive-${cert}"; value = delayService; }
361 );
362 servicesAttr = listToAttrs services;
363 injectServiceDep = {
364 after = [ "acme-selfsigned-certificates.target" ];
365 wants = [ "acme-selfsigned-certificates.target" "acme-certificates.target" ];
366 };
367 in
368 servicesAttr //
369 (if config.services.nginx.enable then { nginx = injectServiceDep; } else {}) //
370 (if config.services.lighttpd.enable then { lighttpd = injectServiceDep; } else {});
372 systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair
373 ("acme-${cert}")
374 ({
375 description = "Renew ACME Certificate for ${cert}";
376 wantedBy = [ "timers.target" ];
377 timerConfig = {
378 OnCalendar = cfg.renewInterval;
379 Unit = "acme-${cert}.service";
380 Persistent = "yes";
381 AccuracySec = "5m";
382 RandomizedDelaySec = "1h";
383 };
384 })
385 );
387 systemd.targets."acme-selfsigned-certificates" = mkIf cfg.preliminarySelfsigned {};
388 systemd.targets."acme-certificates" = {};
389 })
391 ];
393 meta = {
394 maintainers = with lib.maintainers; [ abbradar fpletz globin ];
395 #doc = ./acme.xml; ### CHANGED
396 };