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