1{
2 config,
3 lib,
4 pkgs,
5 options,
6 ...
7}:
8let
9
10 cfg = config.security.acme;
11 opt = options.security.acme;
12 user = if cfg.useRoot then "root" else "acme";
13
14 # Used to calculate timer accuracy for coalescing
15 numCerts = lib.length (builtins.attrNames cfg.certs);
16 _24hSecs = 60 * 60 * 24;
17
18 # Used to make unique paths for each cert/account config set
19 mkHash = with builtins; val: lib.substring 0 20 (hashString "sha256" val);
20 mkAccountHash = acmeServer: data: mkHash "${toString acmeServer} ${data.keyType} ${data.email}";
21 accountDirRoot = "/var/lib/acme/.lego/accounts/";
22
23 # Lockdir is acme-setup.service's RuntimeDirectory.
24 # Since that service is a oneshot with RemainAfterExit,
25 # the folder will exist during all renewal services.
26 lockdir = "/run/acme/";
27 concurrencyLockfiles = map (n: "${toString n}.lock") (lib.range 1 cfg.maxConcurrentRenewals);
28 # Assign elements of `baseList` to each element of `needAssignmentList`, until the latter is exhausted.
29 # returns: [{fst = "element of baseList"; snd = "element of needAssignmentList"}]
30 roundRobinAssign =
31 baseList: needAssignmentList:
32 if baseList == [ ] then [ ] else _rrCycler baseList baseList needAssignmentList;
33 _rrCycler =
34 with builtins;
35 origBaseList: workingBaseList: needAssignmentList:
36 if (workingBaseList == [ ] || needAssignmentList == [ ]) then
37 [ ]
38 else
39 [
40 {
41 fst = head workingBaseList;
42 snd = head needAssignmentList;
43 }
44 ]
45 ++ _rrCycler origBaseList (
46 if (tail workingBaseList == [ ]) then origBaseList else tail workingBaseList
47 ) (tail needAssignmentList);
48 attrsToList = lib.mapAttrsToList (
49 attrname: attrval: {
50 name = attrname;
51 value = attrval;
52 }
53 );
54 # for an AttrSet `funcsAttrs` having functions as values, apply single arguments from
55 # `argsList` to them in a round-robin manner.
56 # Returns an attribute set with the applied functions as values.
57 roundRobinApplyAttrs =
58 funcsAttrs: argsList:
59 lib.listToAttrs (
60 map (x: {
61 inherit (x.snd) name;
62 value = x.snd.value x.fst;
63 }) (roundRobinAssign argsList (attrsToList funcsAttrs))
64 );
65 wrapInFlock =
66 lockfilePath: script:
67 # explainer: https://stackoverflow.com/a/60896531
68 ''
69 exec {LOCKFD}> ${lockfilePath}
70 echo "Waiting to acquire lock ${lockfilePath}"
71 ${pkgs.flock}/bin/flock ''${LOCKFD} || exit 1
72 echo "Acquired lock ${lockfilePath}"
73 ''
74 + script
75 + "\n"
76 + ''echo "Releasing lock ${lockfilePath}" # only released after process exit'';
77
78 # There are many services required to make cert renewals work.
79 # They all follow a common structure:
80 # - They inherit this commonServiceConfig
81 # - They all run as the acme user
82 # - They all use BindPath and StateDirectory where possible
83 # to set up a sort of build environment in /tmp
84 # The Group can vary depending on what the user has specified in
85 # security.acme.certs.<cert>.group on some of the services.
86 commonServiceConfig = {
87 Type = "oneshot";
88 User = user;
89 Group = lib.mkDefault "acme";
90 UMask = "0022";
91 StateDirectoryMode = "750";
92 ProtectSystem = "strict";
93 ReadWritePaths = [
94 "/var/lib/acme"
95 lockdir
96 ];
97 PrivateTmp = true;
98
99 WorkingDirectory = "/tmp";
100
101 CapabilityBoundingSet = [ "" ];
102 DevicePolicy = "closed";
103 LockPersonality = true;
104 MemoryDenyWriteExecute = true;
105 NoNewPrivileges = true;
106 PrivateDevices = true;
107 ProtectClock = true;
108 ProtectHome = true;
109 ProtectHostname = true;
110 ProtectControlGroups = true;
111 ProtectKernelLogs = true;
112 ProtectKernelModules = true;
113 ProtectKernelTunables = true;
114 ProtectProc = "invisible";
115 ProcSubset = "pid";
116 RemoveIPC = true;
117 RestrictAddressFamilies = [
118 "AF_INET"
119 "AF_INET6"
120 "AF_UNIX"
121 "AF_NETLINK"
122 ];
123 RestrictNamespaces = true;
124 RestrictRealtime = true;
125 RestrictSUIDSGID = true;
126 SystemCallArchitectures = "native";
127 SystemCallFilter = [
128 # 1. allow a reasonable set of syscalls
129 "@system-service @resources"
130 # 2. and deny unreasonable ones
131 "~@privileged"
132 # 3. then allow the required subset within denied groups
133 "@chown"
134 ];
135 };
136
137 # Ensures that directories which are shared across all certs
138 # exist and have the correct user and group, since group
139 # is configurable on a per-cert basis.
140 # writeShellScriptBin is used as it produces a nicer binary name, which
141 # journalctl will show when the service is running.
142 privilegedSetupScript = pkgs.writeShellScriptBin "acme-setup-privileged" (
143 ''
144 ${lib.optionalString cfg.defaults.enableDebugLogs "set -x"}
145 set -euo pipefail
146 cd /var/lib/acme
147 chmod -R u=rwX,g=,o= .lego/accounts
148 chown -R ${user} .lego/accounts
149 ''
150 + (lib.concatStringsSep "\n" (
151 lib.mapAttrsToList (cert: data: ''
152 for fixpath in ${lib.escapeShellArg cert} .lego/${lib.escapeShellArg cert}; do
153 if [ -d "$fixpath" ]; then
154 chmod -R u=rwX,g=rX,o= "$fixpath"
155 chown -R ${user}:${data.group} "$fixpath"
156 fi
157 done
158 '') certConfigs
159 ))
160 );
161
162 # This is defined with lib.mkMerge so that we can separate the config per function.
163 setupService = lib.mkMerge [
164 {
165 description = "Set up the ACME certificate renewal infrastructure";
166 script = lib.mkBefore ''
167 ${lib.optionalString cfg.defaults.enableDebugLogs "set -x"}
168 set -euo pipefail
169 '';
170 serviceConfig = commonServiceConfig // {
171 # This script runs with elevated privileges, denoted by the +
172 # ExecStartPre is used instead of ExecStart so that the `script` continues to work.
173 ExecStartPre = "+${lib.getExe privilegedSetupScript}";
174
175 # We don't want this to run every time a renewal happens
176 RemainAfterExit = true;
177
178 # StateDirectory entries are a cleaner, service-level mechanism
179 # for dealing with persistent service data
180 StateDirectory = [
181 "acme"
182 "acme/.lego"
183 "acme/.lego/accounts"
184 ];
185 StateDirectoryMode = "0755";
186
187 # Creates ${lockdir}. Earlier RemainAfterExit=true means
188 # it does not get deleted immediately.
189 RuntimeDirectory = "acme";
190 RuntimeDirectoryMode = "0700";
191
192 # Generally, we don't write anything that should be group accessible.
193 # Group varies for most ACME units, and setup files are only used
194 # under the acme user.
195 UMask = "0077";
196 };
197 }
198
199 # Avoid race conditions creating the CA for selfsigned certs
200 (lib.mkIf cfg.preliminarySelfsigned {
201 path = [ pkgs.minica ];
202 # Working directory will be /tmp
203 script = ''
204 test -e ca/key.pem || minica \
205 --ca-key ca/key.pem \
206 --ca-cert ca/cert.pem \
207 --domains selfsigned.local
208 '';
209 serviceConfig = {
210 StateDirectory = [ "acme/.minica" ];
211 BindPaths = "/var/lib/acme/.minica:/tmp/ca";
212 };
213 })
214 ];
215
216 certToConfig =
217 cert: data:
218 let
219 acmeServer = data.server;
220 useDns = data.dnsProvider != null;
221 destPath = "/var/lib/acme/${cert}";
222 selfsignedDeps = lib.optionals (cfg.preliminarySelfsigned) [ "acme-selfsigned-${cert}.service" ];
223
224 # Minica and lego have a "feature" which replaces * with _. We need
225 # to make this substitution to reference the output files from both programs.
226 # End users never see this since we rename the certs.
227 keyName = builtins.replaceStrings [ "*" ] [ "_" ] data.domain;
228
229 # FIXME when mkChangedOptionModule supports submodules, change to that.
230 # This is a workaround
231 extraDomains =
232 data.extraDomainNames
233 ++ (lib.optionals (data.extraDomains != "_mkMergedOptionModule") (
234 builtins.attrNames data.extraDomains
235 ));
236
237 # Create hashes for cert data directories based on configuration
238 # Flags are separated to avoid collisions
239 hashData = with builtins; ''
240 ${lib.concatStringsSep " " data.extraLegoFlags} -
241 ${lib.concatStringsSep " " data.extraLegoRunFlags} -
242 ${lib.concatStringsSep " " data.extraLegoRenewFlags} -
243 ${toString acmeServer} ${toString data.dnsProvider}
244 ${toString data.ocspMustStaple} ${data.keyType}
245 '';
246 certDir = mkHash hashData;
247 # TODO remove domainHash usage entirely. Waiting on go-acme/lego#1532
248 domainHash = mkHash "${lib.concatStringsSep " " extraDomains} ${data.domain}";
249 accountHash = (mkAccountHash acmeServer data);
250 accountDir = accountDirRoot + accountHash;
251
252 protocolOpts =
253 if useDns then
254 (
255 [
256 "--dns"
257 data.dnsProvider
258 ]
259 ++ lib.optionals (!data.dnsPropagationCheck) [ "--dns.propagation-disable-ans" ]
260 ++ lib.optionals (data.dnsResolver != null) [
261 "--dns.resolvers"
262 data.dnsResolver
263 ]
264 )
265 else if data.s3Bucket != null then
266 [
267 "--http"
268 "--http.s3-bucket"
269 data.s3Bucket
270 ]
271 else if data.listenHTTP != null then
272 [
273 "--http"
274 "--http.port"
275 data.listenHTTP
276 ]
277 else
278 [
279 "--http"
280 "--http.webroot"
281 data.webroot
282 ];
283
284 commonOpts =
285 [
286 "--accept-tos" # Checking the option is covered by the assertions
287 "--path"
288 "."
289 "-d"
290 data.domain
291 "--email"
292 data.email
293 "--key-type"
294 data.keyType
295 ]
296 ++ protocolOpts
297 ++ lib.optionals (acmeServer != null) [
298 "--server"
299 acmeServer
300 ]
301 ++ lib.concatMap (name: [
302 "-d"
303 name
304 ]) extraDomains
305 ++ data.extraLegoFlags;
306
307 # Although --must-staple is common to both modes, it is not declared as a
308 # mode-agnostic argument in lego and thus must come after the mode.
309 runOpts = lib.escapeShellArgs (
310 commonOpts
311 ++ [ "run" ]
312 ++ lib.optionals data.ocspMustStaple [ "--must-staple" ]
313 ++ data.extraLegoRunFlags
314 );
315 renewOpts = lib.escapeShellArgs (
316 commonOpts
317 ++ [
318 "renew"
319 "--no-random-sleep"
320 ]
321 ++ lib.optionals data.ocspMustStaple [ "--must-staple" ]
322 ++ data.extraLegoRenewFlags
323 );
324
325 # We need to collect all the ACME webroots to grant them write
326 # access in the systemd service.
327 webroots = lib.remove null (
328 lib.unique (builtins.map (certAttrs: certAttrs.webroot) (lib.attrValues config.security.acme.certs))
329 );
330 in
331 {
332 inherit accountHash cert selfsignedDeps;
333
334 group = data.group;
335
336 renewTimer = {
337 description = "Renew ACME Certificate for ${cert}";
338 wantedBy = [ "timers.target" ];
339 timerConfig = {
340 OnCalendar = data.renewInterval;
341 Unit = "acme-${cert}.service";
342 Persistent = "yes";
343
344 # Allow systemd to pick a convenient time within the day
345 # to run the check.
346 # This allows the coalescing of multiple timer jobs.
347 # We divide by the number of certificates so that if you
348 # have many certificates, the renewals are distributed over
349 # the course of the day to avoid rate limits.
350 AccuracySec = "${toString (_24hSecs / numCerts)}s";
351 # Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/.
352 RandomizedDelaySec = "24h";
353 FixedRandomDelay = true;
354 };
355 };
356
357 selfsignService = lockfileName: {
358 description = "Generate self-signed certificate for ${cert}";
359 after = [ "acme-setup.service" ];
360 requires = [ "acme-setup.service" ];
361
362 path = [ pkgs.minica ];
363
364 unitConfig = {
365 ConditionPathExists = "!/var/lib/acme/${cert}/key.pem";
366 StartLimitIntervalSec = 0;
367 };
368
369 serviceConfig = commonServiceConfig // {
370 Group = data.group;
371 UMask = "0027";
372
373 StateDirectory = "acme/${cert}";
374
375 BindPaths = [
376 "/var/lib/acme/.minica:/tmp/ca"
377 "/var/lib/acme/${cert}:/tmp/${keyName}"
378 ];
379 };
380
381 # Working directory will be /tmp
382 # minica will output to a folder sharing the name of the first domain
383 # in the list, which will be ${data.domain}
384 script = (if (lockfileName == null) then lib.id else wrapInFlock "${lockdir}${lockfileName}") ''
385 minica \
386 --ca-key ca/key.pem \
387 --ca-cert ca/cert.pem \
388 --domains ${lib.escapeShellArg (builtins.concatStringsSep "," ([ data.domain ] ++ extraDomains))}
389
390 # Create files to match directory layout for real certificates
391 cd '${keyName}'
392 cp ../ca/cert.pem chain.pem
393 cat cert.pem chain.pem > fullchain.pem
394 cat key.pem fullchain.pem > full.pem
395
396 # Group might change between runs, re-apply it
397 chown '${user}:${data.group}' -- *
398
399 # Default permissions make the files unreadable by group + anon
400 # Need to be readable by group
401 chmod 640 -- *
402 '';
403 };
404
405 renewService = lockfileName: {
406 description = "Renew ACME certificate for ${cert}";
407 after = [
408 "network.target"
409 "network-online.target"
410 "acme-setup.service"
411 "nss-lookup.target"
412 ] ++ selfsignedDeps;
413 wants = [ "network-online.target" ] ++ selfsignedDeps;
414 requires = [ "acme-setup.service" ];
415
416 # https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099
417 wantedBy = lib.optionals (!config.boot.isContainer) [ "multi-user.target" ];
418
419 path = with pkgs; [
420 lego
421 coreutils
422 diffutils
423 openssl
424 ];
425
426 serviceConfig =
427 commonServiceConfig
428 // {
429 Group = data.group;
430
431 # Let's Encrypt Failed Validation Limit allows 5 retries per hour, per account, hostname and hour.
432 # This avoids eating them all up if something is misconfigured upon the first try.
433 RestartSec = 15 * 60;
434
435 # Keep in mind that these directories will be deleted if the user runs
436 # systemctl clean --what=state
437 # acme/.lego/${cert} is listed for this reason.
438 StateDirectory = [
439 "acme/${cert}"
440 "acme/.lego/${cert}"
441 "acme/.lego/${cert}/${certDir}"
442 "acme/.lego/accounts/${accountHash}"
443 ];
444
445 ReadWritePaths = commonServiceConfig.ReadWritePaths ++ webroots;
446
447 # Needs to be space separated, but can't use a multiline string because that'll include newlines
448 BindPaths = [
449 "${accountDir}:/tmp/accounts"
450 "/var/lib/acme/${cert}:/tmp/out"
451 "/var/lib/acme/.lego/${cert}/${certDir}:/tmp/certificates"
452 ];
453
454 EnvironmentFile = lib.mkIf (data.environmentFile != null) data.environmentFile;
455
456 Environment = lib.mapAttrsToList (k: v: ''"${k}=%d/${k}"'') data.credentialFiles;
457
458 LoadCredential = lib.mapAttrsToList (k: v: "${k}:${v}") data.credentialFiles;
459
460 # Run as root (Prefixed with +)
461 ExecStartPost =
462 "+"
463 + (pkgs.writeShellScript "acme-postrun" ''
464 cd /var/lib/acme/${lib.escapeShellArg cert}
465 if [ -e renewed ]; then
466 rm renewed
467 ${data.postRun}
468 ${lib.optionalString (
469 data.reloadServices != [ ]
470 ) "systemctl --no-block try-reload-or-restart ${lib.escapeShellArgs data.reloadServices}"}
471 fi
472 '');
473 }
474 //
475 lib.optionalAttrs
476 (data.listenHTTP != null && lib.toInt (lib.last (lib.splitString ":" data.listenHTTP)) < 1024)
477 {
478 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
479 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
480 };
481
482 # Working directory will be /tmp
483 script = (if (lockfileName == null) then lib.id else wrapInFlock "${lockdir}${lockfileName}") ''
484 ${lib.optionalString data.enableDebugLogs "set -x"}
485 set -euo pipefail
486
487 # This reimplements the expiration date check, but without querying
488 # the acme server first. By doing this offline, we avoid errors
489 # when the network or DNS are unavailable, which can happen during
490 # nixos-rebuild switch.
491 is_expiration_skippable() {
492 pem=$1
493
494 # This function relies on set -e to exit early if any of the
495 # conditions or programs fail.
496
497 [[ -e $pem ]]
498
499 expiration_line="$(
500 set -euxo pipefail
501 openssl x509 -noout -enddate <"$pem" \
502 | grep notAfter \
503 | sed -e 's/^notAfter=//'
504 )"
505 [[ -n "$expiration_line" ]]
506
507 expiration_date="$(date -d "$expiration_line" +%s)"
508 now="$(date +%s)"
509 expiration_s=$((expiration_date - now))
510 expiration_days=$((expiration_s / (3600 * 24))) # rounds down
511
512 [[ $expiration_days -gt ${toString data.validMinDays} ]]
513 }
514
515 ${lib.optionalString (data.webroot != null) ''
516 # Ensure the webroot exists. Fixing group is required in case configuration was changed between runs.
517 # Lego will fail if the webroot does not exist at all.
518 (
519 mkdir -p '${data.webroot}/.well-known/acme-challenge' \
520 && chgrp '${data.group}' ${data.webroot}/.well-known/acme-challenge
521 ) || (
522 echo 'Please ensure ${data.webroot}/.well-known/acme-challenge exists and is writable by acme:${data.group}' \
523 && exit 1
524 )
525 ''}
526
527 echo '${domainHash}' > domainhash.txt
528
529 # Check if we can renew.
530 # We can only renew if the list of domains has not changed.
531 # We also need an account key. Avoids #190493
532 if cmp -s domainhash.txt certificates/domainhash.txt && [ -e 'certificates/${keyName}.key' ] && [ -e 'certificates/${keyName}.crt' ] && [ -n "$(find accounts -name '${data.email}.key')" ]; then
533
534 # Even if a cert is not expired, it may be revoked by the CA.
535 # Try to renew, and silently fail if the cert is not expired.
536 # Avoids #85794 and resolves #129838
537 if ! lego ${renewOpts} --days ${toString data.validMinDays}; then
538 if is_expiration_skippable out/full.pem; then
539 echo 1>&2 "nixos-acme: Ignoring failed renewal because expiration isn't within the coming ${toString data.validMinDays} days"
540 else
541 # High number to avoid Systemd reserved codes.
542 exit 11
543 fi
544 fi
545
546 # Otherwise do a full run
547 elif ! lego ${runOpts}; then
548 # Produce a nice error for those doing their first nixos-rebuild with these certs
549 echo Failed to fetch certificates. \
550 This may mean your DNS records are set up incorrectly. \
551 ${lib.optionalString (cfg.preliminarySelfsigned) "Selfsigned certs are in place and dependant services will still start."}
552 # Exit 10 so that users can potentially amend SuccessExitStatus to ignore this error.
553 # High number to avoid Systemd reserved codes.
554 exit 10
555 fi
556
557 mv domainhash.txt certificates/
558
559 # Group might change between runs, re-apply it
560 chown '${user}:${data.group}' certificates/*
561
562 # Copy all certs to the "real" certs directory
563 if ! cmp -s 'certificates/${keyName}.crt' out/fullchain.pem; then
564 touch out/renewed
565 echo Installing new certificate
566 cp -vp 'certificates/${keyName}.crt' out/fullchain.pem
567 cp -vp 'certificates/${keyName}.key' out/key.pem
568 cp -vp 'certificates/${keyName}.issuer.crt' out/chain.pem
569 ln -sf fullchain.pem out/cert.pem
570 cat out/key.pem out/fullchain.pem > out/full.pem
571 fi
572
573 # By default group will have no access to the cert files.
574 # This chmod will fix that.
575 chmod 640 out/*
576
577 # Also ensure safer permissions on the account directory.
578 chmod -R u=rwX,g=,o= accounts/.
579 '';
580 };
581 };
582
583 certConfigs = lib.mapAttrs certToConfig cfg.certs;
584
585 # These options can be specified within
586 # security.acme.defaults or security.acme.certs.<name>
587 inheritableModule =
588 isDefaults:
589 { config, ... }:
590 let
591 defaultAndText = name: default: {
592 # When ! isDefaults then this is the option declaration for the
593 # security.acme.certs.<name> path, which has the extra inheritDefaults
594 # option, which if disabled means that we can't inherit it
595 default = if isDefaults || !config.inheritDefaults then default else cfg.defaults.${name};
596 # The docs however don't need to depend on inheritDefaults, they should
597 # stay constant. Though notably it wouldn't matter much, because to get
598 # the option information, a submodule with name `<name>` is evaluated
599 # without any definitions.
600 defaultText =
601 if isDefaults then default else lib.literalExpression "config.security.acme.defaults.${name}";
602 };
603 in
604 {
605 imports = [
606 (lib.mkRenamedOptionModule [ "credentialsFile" ] [ "environmentFile" ])
607 ];
608
609 options = {
610 validMinDays = lib.mkOption {
611 type = lib.types.int;
612 inherit (defaultAndText "validMinDays" 30) default defaultText;
613 description = "Minimum remaining validity before renewal in days.";
614 };
615
616 renewInterval = lib.mkOption {
617 type = lib.types.str;
618 inherit (defaultAndText "renewInterval" "daily") default defaultText;
619 description = ''
620 Systemd calendar expression when to check for renewal. See
621 {manpage}`systemd.time(7)`.
622 '';
623 };
624
625 enableDebugLogs = lib.mkEnableOption "debug logging for this certificate" // {
626 inherit (defaultAndText "enableDebugLogs" true) default defaultText;
627 };
628
629 webroot = lib.mkOption {
630 type = lib.types.nullOr lib.types.str;
631 inherit (defaultAndText "webroot" null) default defaultText;
632 example = "/var/lib/acme/acme-challenge";
633 description = ''
634 Where the webroot of the HTTP vhost is located.
635 {file}`.well-known/acme-challenge/` directory
636 will be created below the webroot if it doesn't exist.
637 `http://example.org/.well-known/acme-challenge/` must also
638 be available (notice unencrypted HTTP).
639 '';
640 };
641
642 server = lib.mkOption {
643 type = lib.types.nullOr lib.types.str;
644 inherit (defaultAndText "server" "https://acme-v02.api.letsencrypt.org/directory")
645 default
646 defaultText
647 ;
648 example = "https://acme-staging-v02.api.letsencrypt.org/directory";
649 description = ''
650 ACME Directory Resource URI.
651 Defaults to Let's Encrypt's production endpoint.
652 For testing Let's Encrypt's [staging endpoint](https://letsencrypt.org/docs/staging-environment/)
653 should be used to avoid the rather tight rate limit on the production endpoint.
654 '';
655 };
656
657 email = lib.mkOption {
658 type = lib.types.nullOr lib.types.str;
659 inherit (defaultAndText "email" null) default defaultText;
660 description = ''
661 Email address for account creation and correspondence from the CA.
662 It is recommended to use the same email for all certs to avoid account
663 creation limits.
664 '';
665 };
666
667 group = lib.mkOption {
668 type = lib.types.str;
669 inherit (defaultAndText "group" "acme") default defaultText;
670 description = "Group running the ACME client.";
671 };
672
673 reloadServices = lib.mkOption {
674 type = lib.types.listOf lib.types.str;
675 inherit (defaultAndText "reloadServices" [ ]) default defaultText;
676 description = ''
677 The list of systemd services to call `systemctl try-reload-or-restart`
678 on.
679 '';
680 };
681
682 postRun = lib.mkOption {
683 type = lib.types.lines;
684 inherit (defaultAndText "postRun" "") default defaultText;
685 example = "cp full.pem backup.pem";
686 description = ''
687 Commands to run after new certificates go live. Note that
688 these commands run as the root user.
689
690 Executed in the same directory with the new certificate.
691 '';
692 };
693
694 keyType = lib.mkOption {
695 type = lib.types.str;
696 inherit (defaultAndText "keyType" "ec256") default defaultText;
697 description = ''
698 Key type to use for private keys.
699 For an up to date list of supported values check the --key-type option
700 at <https://go-acme.github.io/lego/usage/cli/options/>.
701 '';
702 };
703
704 listenHTTP = lib.mkOption {
705 type = lib.types.nullOr lib.types.str;
706 inherit (defaultAndText "listenHTTP" null) default defaultText;
707 example = ":1360";
708 description = ''
709 Interface and port to listen on to solve HTTP challenges
710 in the form `[INTERFACE]:PORT`.
711 If you use a port other than 80, you must proxy port 80 to this port.
712 '';
713 };
714
715 dnsProvider = lib.mkOption {
716 type = lib.types.nullOr lib.types.str;
717 inherit (defaultAndText "dnsProvider" null) default defaultText;
718 example = "route53";
719 description = ''
720 DNS Challenge provider. For a list of supported providers, see the "code"
721 field of the DNS providers listed at <https://go-acme.github.io/lego/dns/>.
722 '';
723 };
724
725 dnsResolver = lib.mkOption {
726 type = lib.types.nullOr lib.types.str;
727 inherit (defaultAndText "dnsResolver" null) default defaultText;
728 example = "1.1.1.1:53";
729 description = ''
730 Set the resolver to use for performing recursive DNS queries. Supported:
731 host:port. The default is to use the system resolvers, or Google's DNS
732 resolvers if the system's cannot be determined.
733 '';
734 };
735
736 environmentFile = lib.mkOption {
737 type = lib.types.nullOr lib.types.path;
738 inherit (defaultAndText "environmentFile" null) default defaultText;
739 description = ''
740 Path to an EnvironmentFile for the cert's service containing any required and
741 optional environment variables for your selected dnsProvider.
742 To find out what values you need to set, consult the documentation at
743 <https://go-acme.github.io/lego/dns/> for the corresponding dnsProvider.
744 '';
745 example = "/var/src/secrets/example.org-route53-api-token";
746 };
747
748 credentialFiles = lib.mkOption {
749 type = lib.types.attrsOf (lib.types.path);
750 inherit (defaultAndText "credentialFiles" { }) default defaultText;
751 description = ''
752 Environment variables suffixed by "_FILE" to set for the cert's service
753 for your selected dnsProvider.
754 To find out what values you need to set, consult the documentation at
755 <https://go-acme.github.io/lego/dns/> for the corresponding dnsProvider.
756 This allows to securely pass credential files to lego by leveraging systemd
757 credentials.
758 '';
759 example = lib.literalExpression ''
760 {
761 "RFC2136_TSIG_SECRET_FILE" = "/run/secrets/tsig-secret-example.org";
762 }
763 '';
764 };
765
766 dnsPropagationCheck = lib.mkOption {
767 type = lib.types.bool;
768 inherit (defaultAndText "dnsPropagationCheck" true) default defaultText;
769 description = ''
770 Toggles lego DNS propagation check, which is used alongside DNS-01
771 challenge to ensure the DNS entries required are available.
772 '';
773 };
774
775 ocspMustStaple = lib.mkOption {
776 type = lib.types.bool;
777 inherit (defaultAndText "ocspMustStaple" false) default defaultText;
778 description = ''
779 Turns on the OCSP Must-Staple TLS extension.
780 Make sure you know what you're doing! See:
781
782 - <https://blog.apnic.net/2019/01/15/is-the-web-ready-for-ocsp-must-staple/>
783 - <https://blog.hboeck.de/archives/886-The-Problem-with-OCSP-Stapling-and-Must-Staple-and-why-Certificate-Revocation-is-still-broken.html>
784 '';
785 };
786
787 extraLegoFlags = lib.mkOption {
788 type = lib.types.listOf lib.types.str;
789 inherit (defaultAndText "extraLegoFlags" [ ]) default defaultText;
790 description = ''
791 Additional global flags to pass to all lego commands.
792 '';
793 };
794
795 extraLegoRenewFlags = lib.mkOption {
796 type = lib.types.listOf lib.types.str;
797 inherit (defaultAndText "extraLegoRenewFlags" [ ]) default defaultText;
798 description = ''
799 Additional flags to pass to lego renew.
800 '';
801 };
802
803 extraLegoRunFlags = lib.mkOption {
804 type = lib.types.listOf lib.types.str;
805 inherit (defaultAndText "extraLegoRunFlags" [ ]) default defaultText;
806 description = ''
807 Additional flags to pass to lego run.
808 '';
809 };
810 };
811 };
812
813 certOpts =
814 { name, config, ... }:
815 {
816 options = {
817 # user option has been removed
818 user = lib.mkOption {
819 visible = false;
820 default = "_mkRemovedOptionModule";
821 };
822
823 # allowKeysForGroup option has been removed
824 allowKeysForGroup = lib.mkOption {
825 visible = false;
826 default = "_mkRemovedOptionModule";
827 };
828
829 # extraDomains was replaced with extraDomainNames
830 extraDomains = lib.mkOption {
831 visible = false;
832 default = "_mkMergedOptionModule";
833 };
834
835 directory = lib.mkOption {
836 type = lib.types.str;
837 readOnly = true;
838 default = "/var/lib/acme/${name}";
839 description = "Directory where certificate and other state is stored.";
840 };
841
842 domain = lib.mkOption {
843 type = lib.types.str;
844 default = name;
845 description = "Domain to fetch certificate for (defaults to the entry name).";
846 };
847
848 extraDomainNames = lib.mkOption {
849 type = lib.types.listOf lib.types.str;
850 default = [ ];
851 example = lib.literalExpression ''
852 [
853 "example.org"
854 "mydomain.org"
855 ]
856 '';
857 description = ''
858 A list of extra domain names, which are included in the one certificate to be issued.
859 '';
860 };
861
862 s3Bucket = lib.mkOption {
863 type = lib.types.nullOr lib.types.str;
864 default = null;
865 example = "acme";
866 description = ''
867 S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.
868 '';
869 };
870
871 inheritDefaults = lib.mkOption {
872 default = true;
873 example = true;
874 description = "Whether to inherit values set in `security.acme.defaults` or not.";
875 type = lib.types.bool;
876 };
877 };
878 };
879
880in
881{
882
883 options = {
884 security.acme = {
885 preliminarySelfsigned = lib.mkOption {
886 type = lib.types.bool;
887 default = true;
888 description = ''
889 Whether a preliminary self-signed certificate should be generated before
890 doing ACME requests. This can be useful when certificates are required in
891 a webserver, but ACME needs the webserver to make its requests.
892
893 With preliminary self-signed certificate the webserver can be started and
894 can later reload the correct ACME certificates.
895 '';
896 };
897
898 acceptTerms = lib.mkOption {
899 type = lib.types.bool;
900 default = false;
901 description = ''
902 Accept the CA's terms of service. The default provider is Let's Encrypt,
903 you can find their ToS at <https://letsencrypt.org/repository/>.
904 '';
905 };
906
907 useRoot = lib.mkOption {
908 type = lib.types.bool;
909 default = false;
910 description = ''
911 Whether to use the root user when generating certs. This is not recommended
912 for security + compatibility reasons. If a service requires root owned certificates
913 consider following the guide on "Using ACME with services demanding root
914 owned certificates" in the NixOS manual, and only using this as a fallback
915 or for testing.
916 '';
917 };
918
919 defaults = lib.mkOption {
920 type = lib.types.submodule (inheritableModule true);
921 description = ''
922 Default values inheritable by all configured certs. You can
923 use this to define options shared by all your certs. These defaults
924 can also be ignored on a per-cert basis using the
925 {option}`security.acme.certs.''${cert}.inheritDefaults` option.
926 '';
927 };
928
929 certs = lib.mkOption {
930 default = { };
931 type =
932 with lib.types;
933 attrsOf (submodule [
934 (inheritableModule false)
935 certOpts
936 ]);
937 description = ''
938 Attribute set of certificates to get signed and renewed. Creates
939 `acme-''${cert}.{service,timer}` systemd units for
940 each certificate defined here. Other services can add dependencies
941 to those units if they rely on the certificates being present,
942 or trigger restarts of the service if certificates get renewed.
943 '';
944 example = lib.literalExpression ''
945 {
946 "example.com" = {
947 webroot = "/var/lib/acme/acme-challenge/";
948 email = "foo@example.com";
949 extraDomainNames = [ "www.example.com" "foo.example.com" ];
950 };
951 "bar.example.com" = {
952 webroot = "/var/lib/acme/acme-challenge/";
953 email = "bar@example.com";
954 };
955 }
956 '';
957 };
958 maxConcurrentRenewals = lib.mkOption {
959 default = 5;
960 type = lib.types.int;
961 description = ''
962 Maximum number of concurrent certificate generation or renewal jobs. All other
963 jobs will queue and wait running jobs to finish. Reduces the system load of
964 certificate generation.
965
966 Set to `0` to allow unlimited number of concurrent job runs."
967 '';
968 };
969 };
970 };
971
972 imports = [
973 (lib.mkRemovedOptionModule [ "security" "acme" "production" ] ''
974 Use security.acme.server to define your staging ACME server URL instead.
975
976 To use the let's encrypt staging server, use security.acme.server =
977 "https://acme-staging-v02.api.letsencrypt.org/directory".
978 '')
979 (lib.mkRemovedOptionModule [ "security" "acme" "directory" ]
980 "ACME Directory is now hardcoded to /var/lib/acme and its permissions are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info."
981 )
982 (lib.mkRemovedOptionModule [ "security" "acme" "preDelay" ]
983 "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"
984 )
985 (lib.mkRemovedOptionModule [ "security" "acme" "activationDelay" ]
986 "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"
987 )
988 (lib.mkChangedOptionModule
989 [ "security" "acme" "validMin" ]
990 [ "security" "acme" "defaults" "validMinDays" ]
991 (config: config.security.acme.validMin / (24 * 3600))
992 )
993 (lib.mkChangedOptionModule
994 [ "security" "acme" "validMinDays" ]
995 [ "security" "acme" "defaults" "validMinDays" ]
996 (config: config.security.acme.validMinDays)
997 )
998 (lib.mkChangedOptionModule
999 [ "security" "acme" "renewInterval" ]
1000 [ "security" "acme" "defaults" "renewInterval" ]
1001 (config: config.security.acme.renewInterval)
1002 )
1003 (lib.mkChangedOptionModule [ "security" "acme" "email" ] [ "security" "acme" "defaults" "email" ] (
1004 config: config.security.acme.email
1005 ))
1006 (lib.mkChangedOptionModule [ "security" "acme" "server" ] [ "security" "acme" "defaults" "server" ]
1007 (config: config.security.acme.server)
1008 )
1009 (lib.mkChangedOptionModule
1010 [ "security" "acme" "enableDebugLogs" ]
1011 [ "security" "acme" "defaults" "enableDebugLogs" ]
1012 (config: config.security.acme.enableDebugLogs)
1013 )
1014 ];
1015
1016 config = lib.mkMerge [
1017 (lib.mkIf (cfg.certs != { }) {
1018
1019 # FIXME Most of these custom warnings and filters for security.acme.certs.* are required
1020 # because using mkRemovedOptionModule/mkChangedOptionModule with attrsets isn't possible.
1021 warnings = lib.filter (w: w != "") (
1022 lib.mapAttrsToList (
1023 cert: data:
1024 lib.optionalString (data.extraDomains != "_mkMergedOptionModule") ''
1025 The option definition `security.acme.certs.${cert}.extraDomains` has changed
1026 to `security.acme.certs.${cert}.extraDomainNames` and is now a list of strings.
1027 Setting a custom webroot for extra domains is not possible, instead use separate certs.
1028 ''
1029 ) cfg.certs
1030 );
1031
1032 assertions =
1033 let
1034 certs = lib.attrValues cfg.certs;
1035 in
1036 [
1037 {
1038 assertion = cfg.defaults.email != null || lib.all (certOpts: certOpts.email != null) certs;
1039 message = ''
1040 You must define `security.acme.certs.<name>.email` or
1041 `security.acme.defaults.email` to register with the CA. Note that using
1042 many different addresses for certs may trigger account rate limits.
1043 '';
1044 }
1045 {
1046 assertion = cfg.acceptTerms;
1047 message = ''
1048 You must accept the CA's terms of service before using
1049 the ACME module by setting `security.acme.acceptTerms`
1050 to `true`. For Let's Encrypt's ToS see https://letsencrypt.org/repository/
1051 '';
1052 }
1053 ]
1054 ++ (builtins.concatLists (
1055 lib.mapAttrsToList (cert: data: [
1056 {
1057 assertion = data.user == "_mkRemovedOptionModule";
1058 message = ''
1059 The option definition `security.acme.certs.${cert}.user' no longer has any effect; Please remove it.
1060 Certificate user is now hard coded to the "acme" user. If you would
1061 like another user to have access, consider adding them to the
1062 "acme" group or changing security.acme.certs.${cert}.group.
1063 '';
1064 }
1065 {
1066 assertion = data.allowKeysForGroup == "_mkRemovedOptionModule";
1067 message = ''
1068 The option definition `security.acme.certs.${cert}.allowKeysForGroup' no longer has any effect; Please remove it.
1069 All certs are readable by the configured group. If this is undesired,
1070 consider changing security.acme.certs.${cert}.group to an unused group.
1071 '';
1072 }
1073 # * in the cert value breaks building of systemd services, and makes
1074 # referencing them as a user quite weird too. Best practice is to use
1075 # the domain option.
1076 {
1077 assertion = !lib.hasInfix "*" cert;
1078 message = ''
1079 The cert option path `security.acme.certs.${cert}.dnsProvider`
1080 cannot contain a * character.
1081 Instead, set `security.acme.certs.${cert}.domain = "${cert}";`
1082 and remove the wildcard from the path.
1083 '';
1084 }
1085 (
1086 let
1087 exclusiveAttrs = {
1088 inherit (data)
1089 dnsProvider
1090 webroot
1091 listenHTTP
1092 s3Bucket
1093 ;
1094 };
1095 in
1096 {
1097 assertion = lib.length (lib.filter (x: x != null) (builtins.attrValues exclusiveAttrs)) == 1;
1098 message = ''
1099 Exactly one of the options
1100 `security.acme.certs.${cert}.dnsProvider`,
1101 `security.acme.certs.${cert}.webroot`,
1102 `security.acme.certs.${cert}.listenHTTP` and
1103 `security.acme.certs.${cert}.s3Bucket`
1104 is required.
1105 Current values: ${(lib.generators.toPretty { } exclusiveAttrs)}.
1106 '';
1107 }
1108 )
1109 {
1110 assertion = lib.all (lib.hasSuffix "_FILE") (lib.attrNames data.credentialFiles);
1111 message = ''
1112 Option `security.acme.certs.${cert}.credentialFiles` can only be
1113 used for variables suffixed by "_FILE".
1114 '';
1115 }
1116 ]) cfg.certs
1117 ));
1118
1119 users.users.acme = {
1120 home = "/var/lib/acme";
1121 homeMode = "755";
1122 group = "acme";
1123 isSystemUser = true;
1124 };
1125
1126 users.groups.acme = { };
1127
1128 systemd.services =
1129 let
1130 renewServiceFunctions = lib.mapAttrs' (
1131 cert: conf: lib.nameValuePair "acme-${cert}" conf.renewService
1132 ) certConfigs;
1133 renewServices =
1134 if cfg.maxConcurrentRenewals > 0 then
1135 roundRobinApplyAttrs renewServiceFunctions concurrencyLockfiles
1136 else
1137 lib.mapAttrs (_: f: f null) renewServiceFunctions;
1138 selfsignServiceFunctions = lib.mapAttrs' (
1139 cert: conf: lib.nameValuePair "acme-selfsigned-${cert}" conf.selfsignService
1140 ) certConfigs;
1141 selfsignServices =
1142 if cfg.maxConcurrentRenewals > 0 then
1143 roundRobinApplyAttrs selfsignServiceFunctions concurrencyLockfiles
1144 else
1145 lib.mapAttrs (_: f: f null) selfsignServiceFunctions;
1146 in
1147 {
1148 acme-setup = setupService;
1149 }
1150 // renewServices
1151 // lib.optionalAttrs cfg.preliminarySelfsigned selfsignServices;
1152
1153 systemd.timers = lib.mapAttrs' (
1154 cert: conf: lib.nameValuePair "acme-${cert}" conf.renewTimer
1155 ) certConfigs;
1156
1157 systemd.targets =
1158 let
1159 # Create some targets which can be depended on to be "active" after cert renewals
1160 finishedTargets = lib.mapAttrs' (
1161 cert: conf:
1162 lib.nameValuePair "acme-finished-${cert}" {
1163 wantedBy = [ "default.target" ];
1164 requires = [ "acme-${cert}.service" ];
1165 after = [ "acme-${cert}.service" ];
1166 }
1167 ) certConfigs;
1168
1169 # Create targets to limit the number of simultaneous account creations
1170 # How it works:
1171 # - Pick a "leader" cert service, which will be in charge of creating the account,
1172 # and run first (requires + after)
1173 # - Make all other cert services sharing the same account wait for the leader to
1174 # finish before starting (requiredBy + before).
1175 # Using a target here is fine - account creation is a one time event. Even if
1176 # systemd clean --what=state is used to delete the account, so long as the user
1177 # then runs one of the cert services, there won't be any issues.
1178 accountTargets = lib.mapAttrs' (
1179 hash: confs:
1180 let
1181 dnsConfs = builtins.filter (conf: cfg.certs.${conf.cert}.dnsProvider != null) confs;
1182 leaderConf = if dnsConfs != [ ] then builtins.head dnsConfs else builtins.head confs;
1183 leader = "acme-${leaderConf.cert}.service";
1184 followers = map (conf: "acme-${conf.cert}.service") (
1185 builtins.filter (conf: conf != leaderConf) confs
1186 );
1187 in
1188 lib.nameValuePair "acme-account-${hash}" {
1189 requiredBy = followers;
1190 before = followers;
1191 requires = [ leader ];
1192 after = [ leader ];
1193 }
1194 ) (lib.groupBy (conf: conf.accountHash) (lib.attrValues certConfigs));
1195 in
1196 finishedTargets // accountTargets;
1197 })
1198 ];
1199
1200 meta = {
1201 maintainers = lib.teams.acme.members;
1202 doc = ./default.md;
1203 };
1204}