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