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