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 = lib.mdDoc "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 = lib.mdDoc ''
525 Systemd calendar expression when to check for renewal. See
526 {manpage}`systemd.time(7)`.
527 '';
528 };
529
530 enableDebugLogs = mkEnableOption (lib.mdDoc "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 = lib.mdDoc ''
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.nullOr types.str;
549 inherit (defaultAndText "server" null) default defaultText;
550 description = lib.mdDoc ''
551 ACME Directory Resource URI. Defaults to Let's Encrypt's
552 production endpoint,
553 <https://acme-v02.api.letsencrypt.org/directory>, if unset.
554 '';
555 };
556
557 email = mkOption {
558 type = types.nullOr types.str;
559 inherit (defaultAndText "email" null) default defaultText;
560 description = lib.mdDoc ''
561 Email address for account creation and correspondence from the CA.
562 It is recommended to use the same email for all certs to avoid account
563 creation limits.
564 '';
565 };
566
567 group = mkOption {
568 type = types.str;
569 inherit (defaultAndText "group" "acme") default defaultText;
570 description = lib.mdDoc "Group running the ACME client.";
571 };
572
573 reloadServices = mkOption {
574 type = types.listOf types.str;
575 inherit (defaultAndText "reloadServices" []) default defaultText;
576 description = lib.mdDoc ''
577 The list of systemd services to call `systemctl try-reload-or-restart`
578 on.
579 '';
580 };
581
582 postRun = mkOption {
583 type = types.lines;
584 inherit (defaultAndText "postRun" "") default defaultText;
585 example = "cp full.pem backup.pem";
586 description = lib.mdDoc ''
587 Commands to run after new certificates go live. Note that
588 these commands run as the root user.
589
590 Executed in the same directory with the new certificate.
591 '';
592 };
593
594 keyType = mkOption {
595 type = types.str;
596 inherit (defaultAndText "keyType" "ec256") default defaultText;
597 description = lib.mdDoc ''
598 Key type to use for private keys.
599 For an up to date list of supported values check the --key-type option
600 at <https://go-acme.github.io/lego/usage/cli/options/>.
601 '';
602 };
603
604 dnsProvider = mkOption {
605 type = types.nullOr types.str;
606 inherit (defaultAndText "dnsProvider" null) default defaultText;
607 example = "route53";
608 description = lib.mdDoc ''
609 DNS Challenge provider. For a list of supported providers, see the "code"
610 field of the DNS providers listed at <https://go-acme.github.io/lego/dns/>.
611 '';
612 };
613
614 dnsResolver = mkOption {
615 type = types.nullOr types.str;
616 inherit (defaultAndText "dnsResolver" null) default defaultText;
617 example = "1.1.1.1:53";
618 description = lib.mdDoc ''
619 Set the resolver to use for performing recursive DNS queries. Supported:
620 host:port. The default is to use the system resolvers, or Google's DNS
621 resolvers if the system's cannot be determined.
622 '';
623 };
624
625 environmentFile = mkOption {
626 type = types.nullOr types.path;
627 inherit (defaultAndText "environmentFile" null) default defaultText;
628 description = lib.mdDoc ''
629 Path to an EnvironmentFile for the cert's service containing any required and
630 optional environment variables for your selected dnsProvider.
631 To find out what values you need to set, consult the documentation at
632 <https://go-acme.github.io/lego/dns/> for the corresponding dnsProvider.
633 '';
634 example = "/var/src/secrets/example.org-route53-api-token";
635 };
636
637 credentialFiles = mkOption {
638 type = types.attrsOf (types.path);
639 inherit (defaultAndText "credentialFiles" {}) default defaultText;
640 description = lib.mdDoc ''
641 Environment variables suffixed by "_FILE" to set for the cert's service
642 for your selected dnsProvider.
643 To find out what values you need to set, consult the documentation at
644 <https://go-acme.github.io/lego/dns/> for the corresponding dnsProvider.
645 This allows to securely pass credential files to lego by leveraging systemd
646 credentials.
647 '';
648 example = literalExpression ''
649 {
650 "RFC2136_TSIG_SECRET_FILE" = "/run/secrets/tsig-secret-example.org";
651 }
652 '';
653 };
654
655 dnsPropagationCheck = mkOption {
656 type = types.bool;
657 inherit (defaultAndText "dnsPropagationCheck" true) default defaultText;
658 description = lib.mdDoc ''
659 Toggles lego DNS propagation check, which is used alongside DNS-01
660 challenge to ensure the DNS entries required are available.
661 '';
662 };
663
664 ocspMustStaple = mkOption {
665 type = types.bool;
666 inherit (defaultAndText "ocspMustStaple" false) default defaultText;
667 description = lib.mdDoc ''
668 Turns on the OCSP Must-Staple TLS extension.
669 Make sure you know what you're doing! See:
670
671 - <https://blog.apnic.net/2019/01/15/is-the-web-ready-for-ocsp-must-staple/>
672 - <https://blog.hboeck.de/archives/886-The-Problem-with-OCSP-Stapling-and-Must-Staple-and-why-Certificate-Revocation-is-still-broken.html>
673 '';
674 };
675
676 extraLegoFlags = mkOption {
677 type = types.listOf types.str;
678 inherit (defaultAndText "extraLegoFlags" []) default defaultText;
679 description = lib.mdDoc ''
680 Additional global flags to pass to all lego commands.
681 '';
682 };
683
684 extraLegoRenewFlags = mkOption {
685 type = types.listOf types.str;
686 inherit (defaultAndText "extraLegoRenewFlags" []) default defaultText;
687 description = lib.mdDoc ''
688 Additional flags to pass to lego renew.
689 '';
690 };
691
692 extraLegoRunFlags = mkOption {
693 type = types.listOf types.str;
694 inherit (defaultAndText "extraLegoRunFlags" []) default defaultText;
695 description = lib.mdDoc ''
696 Additional flags to pass to lego run.
697 '';
698 };
699 };
700 };
701
702 certOpts = { name, config, ... }: {
703 options = {
704 # user option has been removed
705 user = mkOption {
706 visible = false;
707 default = "_mkRemovedOptionModule";
708 };
709
710 # allowKeysForGroup option has been removed
711 allowKeysForGroup = mkOption {
712 visible = false;
713 default = "_mkRemovedOptionModule";
714 };
715
716 # extraDomains was replaced with extraDomainNames
717 extraDomains = mkOption {
718 visible = false;
719 default = "_mkMergedOptionModule";
720 };
721
722 directory = mkOption {
723 type = types.str;
724 readOnly = true;
725 default = "/var/lib/acme/${name}";
726 description = lib.mdDoc "Directory where certificate and other state is stored.";
727 };
728
729 domain = mkOption {
730 type = types.str;
731 default = name;
732 description = lib.mdDoc "Domain to fetch certificate for (defaults to the entry name).";
733 };
734
735 extraDomainNames = mkOption {
736 type = types.listOf types.str;
737 default = [];
738 example = literalExpression ''
739 [
740 "example.org"
741 "mydomain.org"
742 ]
743 '';
744 description = lib.mdDoc ''
745 A list of extra domain names, which are included in the one certificate to be issued.
746 '';
747 };
748
749 # This setting must be different for each configured certificate, otherwise
750 # two or more renewals may fail to bind to the address. Hence, it is not in
751 # the inheritableOpts.
752 listenHTTP = mkOption {
753 type = types.nullOr types.str;
754 default = null;
755 example = ":1360";
756 description = lib.mdDoc ''
757 Interface and port to listen on to solve HTTP challenges
758 in the form [INTERFACE]:PORT.
759 If you use a port other than 80, you must proxy port 80 to this port.
760 '';
761 };
762
763 s3Bucket = mkOption {
764 type = types.nullOr types.str;
765 default = null;
766 example = "acme";
767 description = lib.mdDoc ''
768 S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.
769 '';
770 };
771
772 inheritDefaults = mkOption {
773 default = true;
774 example = true;
775 description = lib.mdDoc "Whether to inherit values set in `security.acme.defaults` or not.";
776 type = lib.types.bool;
777 };
778 };
779 };
780
781in {
782
783 options = {
784 security.acme = {
785 preliminarySelfsigned = mkOption {
786 type = types.bool;
787 default = true;
788 description = lib.mdDoc ''
789 Whether a preliminary self-signed certificate should be generated before
790 doing ACME requests. This can be useful when certificates are required in
791 a webserver, but ACME needs the webserver to make its requests.
792
793 With preliminary self-signed certificate the webserver can be started and
794 can later reload the correct ACME certificates.
795 '';
796 };
797
798 acceptTerms = mkOption {
799 type = types.bool;
800 default = false;
801 description = lib.mdDoc ''
802 Accept the CA's terms of service. The default provider is Let's Encrypt,
803 you can find their ToS at <https://letsencrypt.org/repository/>.
804 '';
805 };
806
807 useRoot = mkOption {
808 type = types.bool;
809 default = false;
810 description = lib.mdDoc ''
811 Whether to use the root user when generating certs. This is not recommended
812 for security + compatibility reasons. If a service requires root owned certificates
813 consider following the guide on "Using ACME with services demanding root
814 owned certificates" in the NixOS manual, and only using this as a fallback
815 or for testing.
816 '';
817 };
818
819 defaults = mkOption {
820 type = types.submodule (inheritableModule true);
821 description = lib.mdDoc ''
822 Default values inheritable by all configured certs. You can
823 use this to define options shared by all your certs. These defaults
824 can also be ignored on a per-cert basis using the
825 {option}`security.acme.certs.''${cert}.inheritDefaults` option.
826 '';
827 };
828
829 certs = mkOption {
830 default = { };
831 type = with types; attrsOf (submodule [ (inheritableModule false) certOpts ]);
832 description = lib.mdDoc ''
833 Attribute set of certificates to get signed and renewed. Creates
834 `acme-''${cert}.{service,timer}` systemd units for
835 each certificate defined here. Other services can add dependencies
836 to those units if they rely on the certificates being present,
837 or trigger restarts of the service if certificates get renewed.
838 '';
839 example = literalExpression ''
840 {
841 "example.com" = {
842 webroot = "/var/lib/acme/acme-challenge/";
843 email = "foo@example.com";
844 extraDomainNames = [ "www.example.com" "foo.example.com" ];
845 };
846 "bar.example.com" = {
847 webroot = "/var/lib/acme/acme-challenge/";
848 email = "bar@example.com";
849 };
850 }
851 '';
852 };
853 maxConcurrentRenewals = mkOption {
854 default = 5;
855 type = types.int;
856 description = lib.mdDoc ''
857 Maximum number of concurrent certificate generation or renewal jobs. All other
858 jobs will queue and wait running jobs to finish. Reduces the system load of
859 certificate generation.
860
861 Set to `0` to allow unlimited number of concurrent job runs."
862 '';
863 };
864 };
865 };
866
867 imports = [
868 (mkRemovedOptionModule [ "security" "acme" "production" ] ''
869 Use security.acme.server to define your staging ACME server URL instead.
870
871 To use the let's encrypt staging server, use security.acme.server =
872 "https://acme-staging-v02.api.letsencrypt.org/directory".
873 '')
874 (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.")
875 (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")
876 (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")
877 (mkChangedOptionModule [ "security" "acme" "validMin" ] [ "security" "acme" "defaults" "validMinDays" ] (config: config.security.acme.validMin / (24 * 3600)))
878 (mkChangedOptionModule [ "security" "acme" "validMinDays" ] [ "security" "acme" "defaults" "validMinDays" ] (config: config.security.acme.validMinDays))
879 (mkChangedOptionModule [ "security" "acme" "renewInterval" ] [ "security" "acme" "defaults" "renewInterval" ] (config: config.security.acme.renewInterval))
880 (mkChangedOptionModule [ "security" "acme" "email" ] [ "security" "acme" "defaults" "email" ] (config: config.security.acme.email))
881 (mkChangedOptionModule [ "security" "acme" "server" ] [ "security" "acme" "defaults" "server" ] (config: config.security.acme.server))
882 (mkChangedOptionModule [ "security" "acme" "enableDebugLogs" ] [ "security" "acme" "defaults" "enableDebugLogs" ] (config: config.security.acme.enableDebugLogs))
883 ];
884
885 config = mkMerge [
886 (mkIf (cfg.certs != { }) {
887
888 # FIXME Most of these custom warnings and filters for security.acme.certs.* are required
889 # because using mkRemovedOptionModule/mkChangedOptionModule with attrsets isn't possible.
890 warnings = filter (w: w != "") (mapAttrsToList (cert: data: optionalString (data.extraDomains != "_mkMergedOptionModule") ''
891 The option definition `security.acme.certs.${cert}.extraDomains` has changed
892 to `security.acme.certs.${cert}.extraDomainNames` and is now a list of strings.
893 Setting a custom webroot for extra domains is not possible, instead use separate certs.
894 '') cfg.certs);
895
896 assertions = let
897 certs = attrValues cfg.certs;
898 in [
899 {
900 assertion = cfg.email != null || all (certOpts: certOpts.email != null) certs;
901 message = ''
902 You must define `security.acme.certs.<name>.email` or
903 `security.acme.email` to register with the CA. Note that using
904 many different addresses for certs may trigger account rate limits.
905 '';
906 }
907 {
908 assertion = cfg.acceptTerms;
909 message = ''
910 You must accept the CA's terms of service before using
911 the ACME module by setting `security.acme.acceptTerms`
912 to `true`. For Let's Encrypt's ToS see https://letsencrypt.org/repository/
913 '';
914 }
915 ] ++ (builtins.concatLists (mapAttrsToList (cert: data: [
916 {
917 assertion = data.user == "_mkRemovedOptionModule";
918 message = ''
919 The option definition `security.acme.certs.${cert}.user' no longer has any effect; Please remove it.
920 Certificate user is now hard coded to the "acme" user. If you would
921 like another user to have access, consider adding them to the
922 "acme" group or changing security.acme.certs.${cert}.group.
923 '';
924 }
925 {
926 assertion = data.allowKeysForGroup == "_mkRemovedOptionModule";
927 message = ''
928 The option definition `security.acme.certs.${cert}.allowKeysForGroup' no longer has any effect; Please remove it.
929 All certs are readable by the configured group. If this is undesired,
930 consider changing security.acme.certs.${cert}.group to an unused group.
931 '';
932 }
933 # * in the cert value breaks building of systemd services, and makes
934 # referencing them as a user quite weird too. Best practice is to use
935 # the domain option.
936 {
937 assertion = ! hasInfix "*" cert;
938 message = ''
939 The cert option path `security.acme.certs.${cert}.dnsProvider`
940 cannot contain a * character.
941 Instead, set `security.acme.certs.${cert}.domain = "${cert}";`
942 and remove the wildcard from the path.
943 '';
944 }
945 (let exclusiveAttrs = {
946 inherit (data) dnsProvider webroot listenHTTP s3Bucket;
947 }; in {
948 assertion = lib.length (lib.filter (x: x != null) (builtins.attrValues exclusiveAttrs)) == 1;
949 message = ''
950 Exactly one of the options
951 `security.acme.certs.${cert}.dnsProvider`,
952 `security.acme.certs.${cert}.webroot`,
953 `security.acme.certs.${cert}.listenHTTP` and
954 `security.acme.certs.${cert}.s3Bucket`
955 is required.
956 Current values: ${(lib.generators.toPretty {} exclusiveAttrs)}.
957 '';
958 })
959 {
960 assertion = all (hasSuffix "_FILE") (attrNames data.credentialFiles);
961 message = ''
962 Option `security.acme.certs.${cert}.credentialFiles` can only be
963 used for variables suffixed by "_FILE".
964 '';
965 }
966 ]) cfg.certs));
967
968 users.users.acme = {
969 home = "/var/lib/acme";
970 group = "acme";
971 isSystemUser = true;
972 };
973
974 users.groups.acme = {};
975
976 # for lock files, still use tmpfiles as they should better reside in /run
977 systemd.tmpfiles.rules = [
978 "d ${lockdir} 0700 ${user} - - -"
979 "Z ${lockdir} 0700 ${user} - - -"
980 ];
981
982 systemd.services = let
983 renewServiceFunctions = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewService) certConfigs;
984 renewServices = if cfg.maxConcurrentRenewals > 0
985 then roundRobinApplyAttrs renewServiceFunctions concurrencyLockfiles
986 else mapAttrs (_: f: f null) renewServiceFunctions;
987 selfsignServiceFunctions = mapAttrs' (cert: conf: nameValuePair "acme-selfsigned-${cert}" conf.selfsignService) certConfigs;
988 selfsignServices = if cfg.maxConcurrentRenewals > 0
989 then roundRobinApplyAttrs selfsignServiceFunctions concurrencyLockfiles
990 else mapAttrs (_: f: f null) selfsignServiceFunctions;
991 in
992 { "acme-fixperms" = userMigrationService; }
993 // (optionalAttrs (cfg.maxConcurrentRenewals > 0) {"acme-lockfiles" = lockfilePrepareService; })
994 // renewServices
995 // (optionalAttrs (cfg.preliminarySelfsigned) ({
996 "acme-selfsigned-ca" = selfsignCAService;
997 } // selfsignServices));
998
999 systemd.timers = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewTimer) certConfigs;
1000
1001 systemd.targets = let
1002 # Create some targets which can be depended on to be "active" after cert renewals
1003 finishedTargets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
1004 wantedBy = [ "default.target" ];
1005 requires = [ "acme-${cert}.service" ];
1006 after = [ "acme-${cert}.service" ];
1007 }) certConfigs;
1008
1009 # Create targets to limit the number of simultaneous account creations
1010 # How it works:
1011 # - Pick a "leader" cert service, which will be in charge of creating the account,
1012 # and run first (requires + after)
1013 # - Make all other cert services sharing the same account wait for the leader to
1014 # finish before starting (requiredBy + before).
1015 # Using a target here is fine - account creation is a one time event. Even if
1016 # systemd clean --what=state is used to delete the account, so long as the user
1017 # then runs one of the cert services, there won't be any issues.
1018 accountTargets = mapAttrs' (hash: confs: let
1019 leader = "acme-${(builtins.head confs).cert}.service";
1020 dependantServices = map (conf: "acme-${conf.cert}.service") (builtins.tail confs);
1021 in nameValuePair "acme-account-${hash}" {
1022 requiredBy = dependantServices;
1023 before = dependantServices;
1024 requires = [ leader ];
1025 after = [ leader ];
1026 }) (groupBy (conf: conf.accountHash) (attrValues certConfigs));
1027 in finishedTargets // accountTargets;
1028 })
1029 ];
1030
1031 meta = {
1032 maintainers = lib.teams.acme.members;
1033 doc = ./default.md;
1034 };
1035}