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