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