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