1 { config, lib, pkgs, options, ... }:
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 == [])
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
39 exec {LOCKFD}> ${lockfilePath}
40 echo "Waiting to acquire lock ${lockfilePath}"
41 ${pkgs.flock}/bin/flock ''${LOCKFD} || exit 1
42 echo "Acquired lock ${lockfilePath}"
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 = {
59 Group = lib.mkDefault "acme";
61 StateDirectoryMode = "750";
62 ProtectSystem = "strict";
69 WorkingDirectory = "/tmp";
71 CapabilityBoundingSet = [ "" ];
72 DevicePolicy = "closed";
73 LockPersonality = true;
74 MemoryDenyWriteExecute = true;
75 NoNewPrivileges = true;
76 PrivateDevices = true;
79 ProtectHostname = true;
80 ProtectControlGroups = true;
81 ProtectKernelLogs = true;
82 ProtectKernelModules = true;
83 ProtectKernelTunables = true;
84 ProtectProc = "invisible";
87 RestrictAddressFamilies = [
91 RestrictNamespaces = true;
92 RestrictRealtime = true;
93 RestrictSUIDSGID = true;
94 SystemCallArchitectures = "native";
96 # 1. allow a reasonable set of syscalls
97 "@system-service @resources"
98 # 2. and deny unreasonable ones
100 # 3. then allow the required subset within denied groups
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 ];
113 ConditionPathExists = "!/var/lib/acme/.minica/key.pem";
114 StartLimitIntervalSec = 0;
117 serviceConfig = commonServiceConfig // {
118 StateDirectory = "acme/.minica";
119 BindPaths = "/var/lib/acme/.minica:/tmp/ca";
123 # Working directory will be /tmp
126 --ca-key ca/key.pem \
127 --ca-cert ca/cert.pem \
128 --domains selfsigned.local
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"
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);
163 lockfilePrepareService = {
164 description = "Manage lock files for acme services";
166 # ensure all required lock files exist, but none more
168 GLOBIGNORE="${lib.concatStringsSep ":" concurrencyLockfiles}"
172 xargs touch <<< "${toString concurrencyLockfiles}"
175 serviceConfig = commonServiceConfig // {
176 # We don't want this to run every time a renewal happens
177 RemainAfterExit = true;
178 WorkingDirectory = lockdir;
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 ++ (
199 (data.extraDomains != "_mkMergedOptionModule")
200 (builtins.attrNames data.extraDomains)
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}
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 ];
227 "--accept-tos" # Checking the option is covered by the assertions
231 "--key-type" data.keyType
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 (
242 ++ lib.optionals data.ocspMustStaple [ "--must-staple" ]
243 ++ data.extraLegoRunFlags
245 renewOpts = lib.escapeShellArgs (
247 ++ [ "renew" "--no-random-sleep" ]
248 ++ lib.optionals data.ocspMustStaple [ "--must-staple" ]
249 ++ data.extraLegoRenewFlags
252 # We need to collect all the ACME webroots to grant them write
253 # access in the systemd service.
258 (certAttrs: certAttrs.webroot)
259 (lib.attrValues config.security.acme.certs)));
261 inherit accountHash cert selfsignedDeps;
266 description = "Renew ACME Certificate for ${cert}";
267 wantedBy = [ "timers.target" ];
269 OnCalendar = data.renewInterval;
270 Unit = "acme-${cert}.service";
273 # Allow systemd to pick a convenient time within the day
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;
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 ];
294 ConditionPathExists = "!/var/lib/acme/${cert}/key.pem";
295 StartLimitIntervalSec = 0;
298 serviceConfig = commonServiceConfig // {
302 StateDirectory = "acme/${cert}";
305 "/var/lib/acme/.minica:/tmp/ca"
306 "/var/lib/acme/${cert}:/tmp/${keyName}"
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}") ''
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
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
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 // {
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.
357 "acme/.lego/${cert}/${certDir}"
358 "acme/.lego/accounts/${accountHash}"
361 ReadWritePaths = commonServiceConfig.ReadWritePaths ++ webroots;
363 # Needs to be space separated, but can't use a multiline string because that'll include newlines
365 "${accountDir}:/tmp/accounts"
366 "/var/lib/acme/${cert}:/tmp/out"
367 "/var/lib/acme/.lego/${cert}/${certDir}:/tmp/certificates"
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
384 ${lib.optionalString (data.reloadServices != [])
385 "systemctl --no-block try-reload-or-restart ${lib.escapeShellArgs data.reloadServices}"
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" ];
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"}
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() {
406 # This function relies on set -e to exit early if any of the
407 # conditions or programs fail.
413 openssl x509 -noout -enddate <"$pem" \
415 | sed -e 's/^notAfter=//'
417 [[ -n "$expiration_line" ]]
419 expiration_date="$(date -d "$expiration_line" +%s)"
421 expiration_s=$((expiration_date - now))
422 expiration_days=$((expiration_s / (3600 * 24))) # rounds down
424 [[ $expiration_days -gt ${toString data.validMinDays} ]]
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.
431 mkdir -p '${data.webroot}/.well-known/acme-challenge' \
432 && chgrp '${data.group}' ${data.webroot}/.well-known/acme-challenge
434 echo 'Please ensure ${data.webroot}/.well-known/acme-challenge exists and is writable by acme:${data.group}' \
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"
453 # High number to avoid Systemd reserved codes.
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.
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
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
485 # By default group will have no access to the cert files.
486 # This chmod will fix that.
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}";
510 (lib.mkRenamedOptionModule [ "credentialsFile" ] [ "environmentFile" ])
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.";
520 renewInterval = lib.mkOption {
521 type = lib.types.str;
522 inherit (defaultAndText "renewInterval" "daily") default defaultText;
524 Systemd calendar expression when to check for renewal. See
525 {manpage}`systemd.time(7)`.
529 enableDebugLogs = lib.mkEnableOption "debug logging for this certificate" // {
530 inherit (defaultAndText "enableDebugLogs" true) default defaultText;
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";
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).
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";
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.
558 email = lib.mkOption {
559 type = lib.types.nullOr lib.types.str;
560 inherit (defaultAndText "email" null) default defaultText;
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
568 group = lib.mkOption {
569 type = lib.types.str;
570 inherit (defaultAndText "group" "acme") default defaultText;
571 description = "Group running the ACME client.";
574 reloadServices = lib.mkOption {
575 type = lib.types.listOf lib.types.str;
576 inherit (defaultAndText "reloadServices" []) default defaultText;
578 The list of systemd services to call `systemctl try-reload-or-restart`
583 postRun = lib.mkOption {
584 type = lib.types.lines;
585 inherit (defaultAndText "postRun" "") default defaultText;
586 example = "cp full.pem backup.pem";
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.
595 keyType = lib.mkOption {
596 type = lib.types.str;
597 inherit (defaultAndText "keyType" "ec256") default defaultText;
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/>.
605 dnsProvider = lib.mkOption {
606 type = lib.types.nullOr lib.types.str;
607 inherit (defaultAndText "dnsProvider" null) default defaultText;
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/>.
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";
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.
626 environmentFile = lib.mkOption {
627 type = lib.types.nullOr lib.types.path;
628 inherit (defaultAndText "environmentFile" null) default defaultText;
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.
635 example = "/var/src/secrets/example.org-route53-api-token";
638 credentialFiles = lib.mkOption {
639 type = lib.types.attrsOf (lib.types.path);
640 inherit (defaultAndText "credentialFiles" {}) default defaultText;
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
649 example = lib.literalExpression ''
651 "RFC2136_TSIG_SECRET_FILE" = "/run/secrets/tsig-secret-example.org";
656 dnsPropagationCheck = lib.mkOption {
657 type = lib.types.bool;
658 inherit (defaultAndText "dnsPropagationCheck" true) default defaultText;
660 Toggles lego DNS propagation check, which is used alongside DNS-01
661 challenge to ensure the DNS entries required are available.
665 ocspMustStaple = lib.mkOption {
666 type = lib.types.bool;
667 inherit (defaultAndText "ocspMustStaple" false) default defaultText;
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>
677 extraLegoFlags = lib.mkOption {
678 type = lib.types.listOf lib.types.str;
679 inherit (defaultAndText "extraLegoFlags" []) default defaultText;
681 Additional global flags to pass to all lego commands.
685 extraLegoRenewFlags = lib.mkOption {
686 type = lib.types.listOf lib.types.str;
687 inherit (defaultAndText "extraLegoRenewFlags" []) default defaultText;
689 Additional flags to pass to lego renew.
693 extraLegoRunFlags = lib.mkOption {
694 type = lib.types.listOf lib.types.str;
695 inherit (defaultAndText "extraLegoRunFlags" []) default defaultText;
697 Additional flags to pass to lego run.
703 certOpts = { name, config, ... }: {
705 # user option has been removed
706 user = lib.mkOption {
708 default = "_mkRemovedOptionModule";
711 # allowKeysForGroup option has been removed
712 allowKeysForGroup = lib.mkOption {
714 default = "_mkRemovedOptionModule";
717 # extraDomains was replaced with extraDomainNames
718 extraDomains = lib.mkOption {
720 default = "_mkMergedOptionModule";
723 directory = lib.mkOption {
724 type = lib.types.str;
726 default = "/var/lib/acme/${name}";
727 description = "Directory where certificate and other state is stored.";
730 domain = lib.mkOption {
731 type = lib.types.str;
733 description = "Domain to fetch certificate for (defaults to the entry name).";
736 extraDomainNames = lib.mkOption {
737 type = lib.types.listOf lib.types.str;
739 example = lib.literalExpression ''
746 A list of extra domain names, which are included in the one certificate to be issued.
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;
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.
764 s3Bucket = lib.mkOption {
765 type = lib.types.nullOr lib.types.str;
769 S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.
773 inheritDefaults = lib.mkOption {
776 description = "Whether to inherit values set in `security.acme.defaults` or not.";
777 type = lib.types.bool;
786 preliminarySelfsigned = lib.mkOption {
787 type = lib.types.bool;
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.
799 acceptTerms = lib.mkOption {
800 type = lib.types.bool;
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/>.
808 useRoot = lib.mkOption {
809 type = lib.types.bool;
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
820 defaults = lib.mkOption {
821 type = lib.types.submodule (inheritableModule true);
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.
830 certs = lib.mkOption {
832 type = with lib.types; attrsOf (submodule [ (inheritableModule false) certOpts ]);
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.
840 example = lib.literalExpression ''
843 webroot = "/var/lib/acme/acme-challenge/";
844 email = "foo@example.com";
845 extraDomainNames = [ "www.example.com" "foo.example.com" ];
847 "bar.example.com" = {
848 webroot = "/var/lib/acme/acme-challenge/";
849 email = "bar@example.com";
854 maxConcurrentRenewals = lib.mkOption {
856 type = lib.types.int;
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."
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".
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))
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.
898 certs = lib.attrValues cfg.certs;
901 assertion = cfg.defaults.email != null || lib.all (certOpts: certOpts.email != null) certs;
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.
909 assertion = cfg.acceptTerms;
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/
916 ] ++ (builtins.concatLists (lib.mapAttrsToList (cert: data: [
918 assertion = data.user == "_mkRemovedOptionModule";
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.
927 assertion = data.allowKeysForGroup == "_mkRemovedOptionModule";
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.
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
938 assertion = ! lib.hasInfix "*" cert;
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.
946 (let exclusiveAttrs = {
947 inherit (data) dnsProvider webroot listenHTTP s3Bucket;
949 assertion = lib.length (lib.filter (x: x != null) (builtins.attrValues exclusiveAttrs)) == 1;
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`
957 Current values: ${(lib.generators.toPretty {} exclusiveAttrs)}.
961 assertion = lib.all (lib.hasSuffix "_FILE") (lib.attrNames data.credentialFiles);
963 Option `security.acme.certs.${cert}.credentialFiles` can only be
964 used for variables suffixed by "_FILE".
970 home = "/var/lib/acme";
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} - - -"
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;
993 { "acme-fixperms" = userMigrationService; }
994 // (lib.optionalAttrs (cfg.maxConcurrentRenewals > 0) {"acme-lockfiles" = lockfilePrepareService; })
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" ];
1010 # Create targets to limit the number of simultaneous account creations
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 ];
1027 }) (lib.groupBy (conf: conf.accountHash) (lib.attrValues certConfigs));
1028 in finishedTargets // accountTargets;
1033 maintainers = lib.teams.acme.members;