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