1{ config, lib, pkgs, options, ... }:
2with lib;
3let
4 cfg = config.security.acme;
5
6 # Used to calculate timer accuracy for coalescing
7 numCerts = length (builtins.attrNames cfg.certs);
8 _24hSecs = 60 * 60 * 24;
9
10 # Used to make unique paths for each cert/account config set
11 mkHash = with builtins; val: substring 0 20 (hashString "sha256" val);
12 mkAccountHash = acmeServer: data: mkHash "${toString acmeServer} ${data.keyType} ${data.email}";
13 accountDirRoot = "/var/lib/acme/.lego/accounts/";
14
15 # There are many services required to make cert renewals work.
16 # They all follow a common structure:
17 # - They inherit this commonServiceConfig
18 # - They all run as the acme user
19 # - They all use BindPath and StateDirectory where possible
20 # to set up a sort of build environment in /tmp
21 # The Group can vary depending on what the user has specified in
22 # security.acme.certs.<cert>.group on some of the services.
23 commonServiceConfig = {
24 Type = "oneshot";
25 User = "acme";
26 Group = mkDefault "acme";
27 UMask = 0022;
28 StateDirectoryMode = 750;
29 ProtectSystem = "full";
30 PrivateTmp = true;
31
32 WorkingDirectory = "/tmp";
33 };
34
35 # In order to avoid race conditions creating the CA for selfsigned certs,
36 # we have a separate service which will create the necessary files.
37 selfsignCAService = {
38 description = "Generate self-signed certificate authority";
39
40 path = with pkgs; [ minica ];
41
42 unitConfig = {
43 ConditionPathExists = "!/var/lib/acme/.minica/key.pem";
44 };
45
46 serviceConfig = commonServiceConfig // {
47 StateDirectory = "acme/.minica";
48 BindPaths = "/var/lib/acme/.minica:/tmp/ca";
49 };
50
51 # Working directory will be /tmp
52 script = ''
53 minica \
54 --ca-key ca/key.pem \
55 --ca-cert ca/cert.pem \
56 --domains selfsigned.local
57
58 chmod 600 ca/*
59 '';
60 };
61
62 # Ensures that directories which are shared across all certs
63 # exist and have the correct user and group, since group
64 # is configurable on a per-cert basis.
65 userMigrationService = let
66 script = with builtins; ''
67 chown -R acme .lego/accounts
68 '' + (concatStringsSep "\n" (mapAttrsToList (cert: data: ''
69 for fixpath in ${escapeShellArg cert} .lego/${escapeShellArg cert}; do
70 if [ -d "$fixpath" ]; then
71 chmod -R u=rwX,g=rX,o= "$fixpath"
72 chown -R acme:${data.group} "$fixpath"
73 fi
74 done
75 '') certConfigs));
76 in {
77 description = "Fix owner and group of all ACME certificates";
78
79 serviceConfig = commonServiceConfig // {
80 # We don't want this to run every time a renewal happens
81 RemainAfterExit = true;
82
83 # These StateDirectory entries negate the need for tmpfiles
84 StateDirectory = [ "acme" "acme/.lego" "acme/.lego/accounts" ];
85 StateDirectoryMode = 755;
86 WorkingDirectory = "/var/lib/acme";
87
88 # Run the start script as root
89 ExecStart = "+" + (pkgs.writeShellScript "acme-fixperms" script);
90 };
91 };
92
93 certToConfig = cert: data: let
94 acmeServer = if data.server != null then data.server else cfg.server;
95 useDns = data.dnsProvider != null;
96 destPath = "/var/lib/acme/${cert}";
97 selfsignedDeps = optionals (cfg.preliminarySelfsigned) [ "acme-selfsigned-${cert}.service" ];
98
99 # Minica and lego have a "feature" which replaces * with _. We need
100 # to make this substitution to reference the output files from both programs.
101 # End users never see this since we rename the certs.
102 keyName = builtins.replaceStrings ["*"] ["_"] data.domain;
103
104 # FIXME when mkChangedOptionModule supports submodules, change to that.
105 # This is a workaround
106 extraDomains = data.extraDomainNames ++ (
107 optionals
108 (data.extraDomains != "_mkMergedOptionModule")
109 (builtins.attrNames data.extraDomains)
110 );
111
112 # Create hashes for cert data directories based on configuration
113 # Flags are separated to avoid collisions
114 hashData = with builtins; ''
115 ${concatStringsSep " " data.extraLegoFlags} -
116 ${concatStringsSep " " data.extraLegoRunFlags} -
117 ${concatStringsSep " " data.extraLegoRenewFlags} -
118 ${toString acmeServer} ${toString data.dnsProvider}
119 ${toString data.ocspMustStaple} ${data.keyType}
120 '';
121 certDir = mkHash hashData;
122 domainHash = mkHash "${concatStringsSep " " extraDomains} ${data.domain}";
123 accountHash = (mkAccountHash acmeServer data);
124 accountDir = accountDirRoot + accountHash;
125
126 protocolOpts = if useDns then (
127 [ "--dns" data.dnsProvider ]
128 ++ optionals (!data.dnsPropagationCheck) [ "--dns.disable-cp" ]
129 ++ optionals (data.dnsResolver != null) [ "--dns.resolvers" data.dnsResolver ]
130 ) else (
131 [ "--http" "--http.webroot" data.webroot ]
132 );
133
134 commonOpts = [
135 "--accept-tos" # Checking the option is covered by the assertions
136 "--path" "."
137 "-d" data.domain
138 "--email" data.email
139 "--key-type" data.keyType
140 ] ++ protocolOpts
141 ++ optionals (acmeServer != null) [ "--server" acmeServer ]
142 ++ concatMap (name: [ "-d" name ]) extraDomains
143 ++ data.extraLegoFlags;
144
145 # Although --must-staple is common to both modes, it is not declared as a
146 # mode-agnostic argument in lego and thus must come after the mode.
147 runOpts = escapeShellArgs (
148 commonOpts
149 ++ [ "run" ]
150 ++ optionals data.ocspMustStaple [ "--must-staple" ]
151 ++ data.extraLegoRunFlags
152 );
153 renewOpts = escapeShellArgs (
154 commonOpts
155 ++ [ "renew" "--reuse-key" ]
156 ++ optionals data.ocspMustStaple [ "--must-staple" ]
157 ++ data.extraLegoRenewFlags
158 );
159
160 in {
161 inherit accountHash cert selfsignedDeps;
162
163 group = data.group;
164
165 renewTimer = {
166 description = "Renew ACME Certificate for ${cert}";
167 wantedBy = [ "timers.target" ];
168 timerConfig = {
169 OnCalendar = cfg.renewInterval;
170 Unit = "acme-${cert}.service";
171 Persistent = "yes";
172
173 # Allow systemd to pick a convenient time within the day
174 # to run the check.
175 # This allows the coalescing of multiple timer jobs.
176 # We divide by the number of certificates so that if you
177 # have many certificates, the renewals are distributed over
178 # the course of the day to avoid rate limits.
179 AccuracySec = "${toString (_24hSecs / numCerts)}s";
180
181 # Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/.
182 RandomizedDelaySec = "24h";
183 };
184 };
185
186 selfsignService = {
187 description = "Generate self-signed certificate for ${cert}";
188 after = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ];
189 requires = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ];
190
191 path = with pkgs; [ minica ];
192
193 unitConfig = {
194 ConditionPathExists = "!/var/lib/acme/${cert}/key.pem";
195 };
196
197 serviceConfig = commonServiceConfig // {
198 Group = data.group;
199
200 StateDirectory = "acme/${cert}";
201
202 BindPaths = [
203 "/var/lib/acme/.minica:/tmp/ca"
204 "/var/lib/acme/${cert}:/tmp/${keyName}"
205 ];
206 };
207
208 # Working directory will be /tmp
209 # minica will output to a folder sharing the name of the first domain
210 # in the list, which will be ${data.domain}
211 script = ''
212 minica \
213 --ca-key ca/key.pem \
214 --ca-cert ca/cert.pem \
215 --domains ${escapeShellArg (builtins.concatStringsSep "," ([ data.domain ] ++ extraDomains))}
216
217 # Create files to match directory layout for real certificates
218 cd '${keyName}'
219 cp ../ca/cert.pem chain.pem
220 cat cert.pem chain.pem > fullchain.pem
221 cat key.pem fullchain.pem > full.pem
222
223 chmod 640 *
224
225 # Group might change between runs, re-apply it
226 chown 'acme:${data.group}' *
227 '';
228 };
229
230 renewService = {
231 description = "Renew ACME certificate for ${cert}";
232 after = [ "network.target" "network-online.target" "acme-fixperms.service" "nss-lookup.target" ] ++ selfsignedDeps;
233 wants = [ "network-online.target" "acme-fixperms.service" ] ++ selfsignedDeps;
234
235 # https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099
236 wantedBy = optionals (!config.boot.isContainer) [ "multi-user.target" ];
237
238 path = with pkgs; [ lego coreutils diffutils openssl ];
239
240 serviceConfig = commonServiceConfig // {
241 Group = data.group;
242
243 # Keep in mind that these directories will be deleted if the user runs
244 # systemctl clean --what=state
245 # acme/.lego/${cert} is listed for this reason.
246 StateDirectory = [
247 "acme/${cert}"
248 "acme/.lego/${cert}"
249 "acme/.lego/${cert}/${certDir}"
250 "acme/.lego/accounts/${accountHash}"
251 ];
252
253 # Needs to be space separated, but can't use a multiline string because that'll include newlines
254 BindPaths = [
255 "${accountDir}:/tmp/accounts"
256 "/var/lib/acme/${cert}:/tmp/out"
257 "/var/lib/acme/.lego/${cert}/${certDir}:/tmp/certificates"
258 ];
259
260 # Only try loading the credentialsFile if the dns challenge is enabled
261 EnvironmentFile = mkIf useDns data.credentialsFile;
262
263 # Run as root (Prefixed with +)
264 ExecStartPost = "+" + (pkgs.writeShellScript "acme-postrun" ''
265 cd /var/lib/acme/${escapeShellArg cert}
266 if [ -e renewed ]; then
267 rm renewed
268 ${data.postRun}
269 fi
270 '');
271 };
272
273 # Working directory will be /tmp
274 script = ''
275 set -euxo pipefail
276
277 # This reimplements the expiration date check, but without querying
278 # the acme server first. By doing this offline, we avoid errors
279 # when the network or DNS are unavailable, which can happen during
280 # nixos-rebuild switch.
281 is_expiration_skippable() {
282 pem=$1
283
284 # This function relies on set -e to exit early if any of the
285 # conditions or programs fail.
286
287 [[ -e $pem ]]
288
289 expiration_line="$(
290 set -euxo pipefail
291 openssl x509 -noout -enddate <$pem \
292 | grep notAfter \
293 | sed -e 's/^notAfter=//'
294 )"
295 [[ -n "$expiration_line" ]]
296
297 expiration_date="$(date -d "$expiration_line" +%s)"
298 now="$(date +%s)"
299 expiration_s=$[expiration_date - now]
300 expiration_days=$[expiration_s / (3600 * 24)] # rounds down
301
302 [[ $expiration_days -gt ${toString cfg.validMinDays} ]]
303 }
304
305 ${optionalString (data.webroot != null) ''
306 # Ensure the webroot exists. Fixing group is required in case configuration was changed between runs.
307 # Lego will fail if the webroot does not exist at all.
308 (
309 mkdir -p '${data.webroot}/.well-known/acme-challenge' \
310 && chgrp '${data.group}' ${data.webroot}/.well-known/acme-challenge
311 ) || (
312 echo 'Please ensure ${data.webroot}/.well-known/acme-challenge exists and is writable by acme:${data.group}' \
313 && exit 1
314 )
315 ''}
316
317 echo '${domainHash}' > domainhash.txt
318
319 # Check if we can renew
320 if [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' -a -n "$(ls -1 accounts)" ]; then
321
322 # When domains are updated, there's no need to do a full
323 # Lego run, but it's likely renew won't work if days is too low.
324 if [ -e certificates/domainhash.txt ] && cmp -s domainhash.txt certificates/domainhash.txt; then
325 if is_expiration_skippable out/full.pem; then
326 echo 1>&2 "nixos-acme: skipping renewal because expiration isn't within the coming ${toString cfg.validMinDays} days"
327 else
328 echo 1>&2 "nixos-acme: renewing now, because certificate expires within the configured ${toString cfg.validMinDays} days"
329 lego ${renewOpts} --days ${toString cfg.validMinDays}
330 fi
331 else
332 echo 1>&2 "certificate domain(s) have changed; will renew now"
333 # Any number > 90 works, but this one is over 9000 ;-)
334 lego ${renewOpts} --days 9001
335 fi
336
337 # Otherwise do a full run
338 else
339 lego ${runOpts}
340 fi
341
342 mv domainhash.txt certificates/
343 chmod 640 certificates/*
344 chmod -R u=rwX,g=,o= accounts/*
345
346 # Group might change between runs, re-apply it
347 chown 'acme:${data.group}' certificates/*
348
349 # Copy all certs to the "real" certs directory
350 CERT='certificates/${keyName}.crt'
351 if [ -e "$CERT" ] && ! cmp -s "$CERT" out/fullchain.pem; then
352 touch out/renewed
353 echo Installing new certificate
354 cp -vp 'certificates/${keyName}.crt' out/fullchain.pem
355 cp -vp 'certificates/${keyName}.key' out/key.pem
356 cp -vp 'certificates/${keyName}.issuer.crt' out/chain.pem
357 ln -sf fullchain.pem out/cert.pem
358 cat out/key.pem out/fullchain.pem > out/full.pem
359 fi
360 '';
361 };
362 };
363
364 certConfigs = mapAttrs certToConfig cfg.certs;
365
366 certOpts = { name, ... }: {
367 options = {
368 # user option has been removed
369 user = mkOption {
370 visible = false;
371 default = "_mkRemovedOptionModule";
372 };
373
374 # allowKeysForGroup option has been removed
375 allowKeysForGroup = mkOption {
376 visible = false;
377 default = "_mkRemovedOptionModule";
378 };
379
380 # extraDomains was replaced with extraDomainNames
381 extraDomains = mkOption {
382 visible = false;
383 default = "_mkMergedOptionModule";
384 };
385
386 webroot = mkOption {
387 type = types.nullOr types.str;
388 default = null;
389 example = "/var/lib/acme/acme-challenge";
390 description = ''
391 Where the webroot of the HTTP vhost is located.
392 <filename>.well-known/acme-challenge/</filename> directory
393 will be created below the webroot if it doesn't exist.
394 <literal>http://example.org/.well-known/acme-challenge/</literal> must also
395 be available (notice unencrypted HTTP).
396 '';
397 };
398
399 server = mkOption {
400 type = types.nullOr types.str;
401 default = null;
402 description = ''
403 ACME Directory Resource URI. Defaults to Let's Encrypt's
404 production endpoint,
405 <link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset.
406 '';
407 };
408
409 domain = mkOption {
410 type = types.str;
411 default = name;
412 description = "Domain to fetch certificate for (defaults to the entry name).";
413 };
414
415 email = mkOption {
416 type = types.nullOr types.str;
417 default = cfg.email;
418 description = "Contact email address for the CA to be able to reach you.";
419 };
420
421 group = mkOption {
422 type = types.str;
423 default = "acme";
424 description = "Group running the ACME client.";
425 };
426
427 postRun = mkOption {
428 type = types.lines;
429 default = "";
430 example = "cp full.pem backup.pem";
431 description = ''
432 Commands to run after new certificates go live. Note that
433 these commands run as the root user.
434
435 Executed in the same directory with the new certificate.
436 '';
437 };
438
439 directory = mkOption {
440 type = types.str;
441 readOnly = true;
442 default = "/var/lib/acme/${name}";
443 description = "Directory where certificate and other state is stored.";
444 };
445
446 extraDomainNames = mkOption {
447 type = types.listOf types.str;
448 default = [];
449 example = literalExample ''
450 [
451 "example.org"
452 "mydomain.org"
453 ]
454 '';
455 description = ''
456 A list of extra domain names, which are included in the one certificate to be issued.
457 '';
458 };
459
460 keyType = mkOption {
461 type = types.str;
462 default = "ec256";
463 description = ''
464 Key type to use for private keys.
465 For an up to date list of supported values check the --key-type option
466 at <link xlink:href="https://go-acme.github.io/lego/usage/cli/#usage"/>.
467 '';
468 };
469
470 dnsProvider = mkOption {
471 type = types.nullOr types.str;
472 default = null;
473 example = "route53";
474 description = ''
475 DNS Challenge provider. For a list of supported providers, see the "code"
476 field of the DNS providers listed at <link xlink:href="https://go-acme.github.io/lego/dns/"/>.
477 '';
478 };
479
480 dnsResolver = mkOption {
481 type = types.nullOr types.str;
482 default = null;
483 example = "1.1.1.1:53";
484 description = ''
485 Set the resolver to use for performing recursive DNS queries. Supported:
486 host:port. The default is to use the system resolvers, or Google's DNS
487 resolvers if the system's cannot be determined.
488 '';
489 };
490
491 credentialsFile = mkOption {
492 type = types.path;
493 description = ''
494 Path to an EnvironmentFile for the cert's service containing any required and
495 optional environment variables for your selected dnsProvider.
496 To find out what values you need to set, consult the documentation at
497 <link xlink:href="https://go-acme.github.io/lego/dns/"/> for the corresponding dnsProvider.
498 '';
499 example = "/var/src/secrets/example.org-route53-api-token";
500 };
501
502 dnsPropagationCheck = mkOption {
503 type = types.bool;
504 default = true;
505 description = ''
506 Toggles lego DNS propagation check, which is used alongside DNS-01
507 challenge to ensure the DNS entries required are available.
508 '';
509 };
510
511 ocspMustStaple = mkOption {
512 type = types.bool;
513 default = false;
514 description = ''
515 Turns on the OCSP Must-Staple TLS extension.
516 Make sure you know what you're doing! See:
517 <itemizedlist>
518 <listitem><para><link xlink:href="https://blog.apnic.net/2019/01/15/is-the-web-ready-for-ocsp-must-staple/" /></para></listitem>
519 <listitem><para><link xlink:href="https://blog.hboeck.de/archives/886-The-Problem-with-OCSP-Stapling-and-Must-Staple-and-why-Certificate-Revocation-is-still-broken.html" /></para></listitem>
520 </itemizedlist>
521 '';
522 };
523
524 extraLegoFlags = mkOption {
525 type = types.listOf types.str;
526 default = [];
527 description = ''
528 Additional global flags to pass to all lego commands.
529 '';
530 };
531
532 extraLegoRenewFlags = mkOption {
533 type = types.listOf types.str;
534 default = [];
535 description = ''
536 Additional flags to pass to lego renew.
537 '';
538 };
539
540 extraLegoRunFlags = mkOption {
541 type = types.listOf types.str;
542 default = [];
543 description = ''
544 Additional flags to pass to lego run.
545 '';
546 };
547 };
548 };
549
550in {
551
552 options = {
553 security.acme = {
554
555 validMinDays = mkOption {
556 type = types.int;
557 default = 30;
558 description = "Minimum remaining validity before renewal in days.";
559 };
560
561 email = mkOption {
562 type = types.nullOr types.str;
563 default = null;
564 description = "Contact email address for the CA to be able to reach you.";
565 };
566
567 renewInterval = mkOption {
568 type = types.str;
569 default = "daily";
570 description = ''
571 Systemd calendar expression when to check for renewal. See
572 <citerefentry><refentrytitle>systemd.time</refentrytitle>
573 <manvolnum>7</manvolnum></citerefentry>.
574 '';
575 };
576
577 server = mkOption {
578 type = types.nullOr types.str;
579 default = null;
580 description = ''
581 ACME Directory Resource URI. Defaults to Let's Encrypt's
582 production endpoint,
583 <link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset.
584 '';
585 };
586
587 preliminarySelfsigned = mkOption {
588 type = types.bool;
589 default = true;
590 description = ''
591 Whether a preliminary self-signed certificate should be generated before
592 doing ACME requests. This can be useful when certificates are required in
593 a webserver, but ACME needs the webserver to make its requests.
594
595 With preliminary self-signed certificate the webserver can be started and
596 can later reload the correct ACME certificates.
597 '';
598 };
599
600 acceptTerms = mkOption {
601 type = types.bool;
602 default = false;
603 description = ''
604 Accept the CA's terms of service. The default provider is Let's Encrypt,
605 you can find their ToS at <link xlink:href="https://letsencrypt.org/repository/"/>.
606 '';
607 };
608
609 certs = mkOption {
610 default = { };
611 type = with types; attrsOf (submodule certOpts);
612 description = ''
613 Attribute set of certificates to get signed and renewed. Creates
614 <literal>acme-''${cert}.{service,timer}</literal> systemd units for
615 each certificate defined here. Other services can add dependencies
616 to those units if they rely on the certificates being present,
617 or trigger restarts of the service if certificates get renewed.
618 '';
619 example = literalExample ''
620 {
621 "example.com" = {
622 webroot = "/var/lib/acme/acme-challenge/";
623 email = "foo@example.com";
624 extraDomainNames = [ "www.example.com" "foo.example.com" ];
625 };
626 "bar.example.com" = {
627 webroot = "/var/lib/acme/acme-challenge/";
628 email = "bar@example.com";
629 };
630 }
631 '';
632 };
633 };
634 };
635
636 imports = [
637 (mkRemovedOptionModule [ "security" "acme" "production" ] ''
638 Use security.acme.server to define your staging ACME server URL instead.
639
640 To use the let's encrypt staging server, use security.acme.server =
641 "https://acme-staging-v02.api.letsencrypt.org/directory".
642 ''
643 )
644 (mkRemovedOptionModule [ "security" "acme" "directory" ] "ACME Directory is now hardcoded to /var/lib/acme and its permisisons are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info.")
645 (mkRemovedOptionModule [ "security" "acme" "preDelay" ] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
646 (mkRemovedOptionModule [ "security" "acme" "activationDelay" ] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
647 (mkChangedOptionModule [ "security" "acme" "validMin" ] [ "security" "acme" "validMinDays" ] (config: config.security.acme.validMin / (24 * 3600)))
648 ];
649
650 config = mkMerge [
651 (mkIf (cfg.certs != { }) {
652
653 # FIXME Most of these custom warnings and filters for security.acme.certs.* are required
654 # because using mkRemovedOptionModule/mkChangedOptionModule with attrsets isn't possible.
655 warnings = filter (w: w != "") (mapAttrsToList (cert: data: if data.extraDomains != "_mkMergedOptionModule" then ''
656 The option definition `security.acme.certs.${cert}.extraDomains` has changed
657 to `security.acme.certs.${cert}.extraDomainNames` and is now a list of strings.
658 Setting a custom webroot for extra domains is not possible, instead use separate certs.
659 '' else "") cfg.certs);
660
661 assertions = let
662 certs = attrValues cfg.certs;
663 in [
664 {
665 assertion = cfg.email != null || all (certOpts: certOpts.email != null) certs;
666 message = ''
667 You must define `security.acme.certs.<name>.email` or
668 `security.acme.email` to register with the CA. Note that using
669 many different addresses for certs may trigger account rate limits.
670 '';
671 }
672 {
673 assertion = cfg.acceptTerms;
674 message = ''
675 You must accept the CA's terms of service before using
676 the ACME module by setting `security.acme.acceptTerms`
677 to `true`. For Let's Encrypt's ToS see https://letsencrypt.org/repository/
678 '';
679 }
680 ] ++ (builtins.concatLists (mapAttrsToList (cert: data: [
681 {
682 assertion = data.user == "_mkRemovedOptionModule";
683 message = ''
684 The option definition `security.acme.certs.${cert}.user' no longer has any effect; Please remove it.
685 Certificate user is now hard coded to the "acme" user. If you would
686 like another user to have access, consider adding them to the
687 "acme" group or changing security.acme.certs.${cert}.group.
688 '';
689 }
690 {
691 assertion = data.allowKeysForGroup == "_mkRemovedOptionModule";
692 message = ''
693 The option definition `security.acme.certs.${cert}.allowKeysForGroup' no longer has any effect; Please remove it.
694 All certs are readable by the configured group. If this is undesired,
695 consider changing security.acme.certs.${cert}.group to an unused group.
696 '';
697 }
698 # * in the cert value breaks building of systemd services, and makes
699 # referencing them as a user quite weird too. Best practice is to use
700 # the domain option.
701 {
702 assertion = ! hasInfix "*" cert;
703 message = ''
704 The cert option path `security.acme.certs.${cert}.dnsProvider`
705 cannot contain a * character.
706 Instead, set `security.acme.certs.${cert}.domain = "${cert}";`
707 and remove the wildcard from the path.
708 '';
709 }
710 {
711 assertion = data.dnsProvider == null || data.webroot == null;
712 message = ''
713 Options `security.acme.certs.${cert}.dnsProvider` and
714 `security.acme.certs.${cert}.webroot` are mutually exclusive.
715 '';
716 }
717 ]) cfg.certs));
718
719 users.users.acme = {
720 home = "/var/lib/acme";
721 group = "acme";
722 isSystemUser = true;
723 };
724
725 users.groups.acme = {};
726
727 systemd.services = {
728 "acme-fixperms" = userMigrationService;
729 } // (mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewService) certConfigs)
730 // (optionalAttrs (cfg.preliminarySelfsigned) ({
731 "acme-selfsigned-ca" = selfsignCAService;
732 } // (mapAttrs' (cert: conf: nameValuePair "acme-selfsigned-${cert}" conf.selfsignService) certConfigs)));
733
734 systemd.timers = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewTimer) certConfigs;
735
736 systemd.targets = let
737 # Create some targets which can be depended on to be "active" after cert renewals
738 finishedTargets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
739 wantedBy = [ "default.target" ];
740 requires = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
741 after = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
742 }) certConfigs;
743
744 # Create targets to limit the number of simultaneous account creations
745 # How it works:
746 # - Pick a "leader" cert service, which will be in charge of creating the account,
747 # and run first (requires + after)
748 # - Make all other cert services sharing the same account wait for the leader to
749 # finish before starting (requiredBy + before).
750 # Using a target here is fine - account creation is a one time event. Even if
751 # systemd clean --what=state is used to delete the account, so long as the user
752 # then runs one of the cert services, there won't be any issues.
753 accountTargets = mapAttrs' (hash: confs: let
754 leader = "acme-${(builtins.head confs).cert}.service";
755 dependantServices = map (conf: "acme-${conf.cert}.service") (builtins.tail confs);
756 in nameValuePair "acme-account-${hash}" {
757 requiredBy = dependantServices;
758 before = dependantServices;
759 requires = [ leader ];
760 after = [ leader ];
761 }) (groupBy (conf: conf.accountHash) (attrValues certConfigs));
762 in finishedTargets // accountTargets;
763 })
764 ];
765
766 meta = {
767 maintainers = lib.teams.acme.members;
768 doc = ./acme.xml;
769 };
770}