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