diff --git a/nixos/modules/security/acme/default.md b/nixos/modules/security/acme/default.md index b3a5f838f897..0766eb0e9a46 100644 --- a/nixos/modules/security/acme/default.md +++ b/nixos/modules/security/acme/default.md @@ -318,7 +318,7 @@ can be applied to any service. # Now you must augment OpenSMTPD's systemd service to load # the certificate files. - systemd.services.opensmtpd.requires = [ "acme-finished-mail.example.com.target" ]; + systemd.services.opensmtpd.requires = [ "acme-mail.example.com.service" ]; systemd.services.opensmtpd.serviceConfig.LoadCredential = let certDir = config.security.acme.certs."mail.example.com".directory; diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix index 92c545797376..f1b7b7b0a032 100644 --- a/nixos/modules/security/acme/default.nix +++ b/nixos/modules/security/acme/default.nix @@ -160,58 +160,49 @@ let ); # This is defined with lib.mkMerge so that we can separate the config per function. - setupService = lib.mkMerge [ - { - description = "Set up the ACME certificate renewal infrastructure"; - script = lib.mkBefore '' - ${lib.optionalString cfg.defaults.enableDebugLogs "set -x"} - set -euo pipefail - ''; - serviceConfig = commonServiceConfig // { - # This script runs with elevated privileges, denoted by the + - # ExecStartPre is used instead of ExecStart so that the `script` continues to work. - ExecStartPre = "+${lib.getExe privilegedSetupScript}"; + setupService = { + description = "Set up the ACME certificate renewal infrastructure"; + path = [ pkgs.minica ]; - # We don't want this to run every time a renewal happens - RemainAfterExit = true; + script = lib.mkBefore '' + ${lib.optionalString cfg.defaults.enableDebugLogs "set -x"} + set -euo pipefail + test -e ca/key.pem || minica \ + --ca-key ca/key.pem \ + --ca-cert ca/cert.pem \ + --domains selfsigned.local + ''; - # StateDirectory entries are a cleaner, service-level mechanism - # for dealing with persistent service data - StateDirectory = [ - "acme" - "acme/.lego" - "acme/.lego/accounts" - ]; - StateDirectoryMode = "0755"; + serviceConfig = commonServiceConfig // { + # This script runs with elevated privileges, denoted by the + + # ExecStartPre is used instead of ExecStart so that the `script` continues to work. + ExecStartPre = "+${lib.getExe privilegedSetupScript}"; - # Creates ${lockdir}. Earlier RemainAfterExit=true means - # it does not get deleted immediately. - RuntimeDirectory = "acme"; - RuntimeDirectoryMode = "0700"; + # We don't want this to run every time a renewal happens + RemainAfterExit = true; - # Generally, we don't write anything that should be group accessible. - # Group varies for most ACME units, and setup files are only used - # under the acme user. - UMask = "0077"; - }; - } + # StateDirectory entries are a cleaner, service-level mechanism + # for dealing with persistent service data + StateDirectory = [ + "acme" + "acme/.lego" + "acme/.lego/accounts" + "acme/.minica" + ]; + BindPaths = "/var/lib/acme/.minica:/tmp/ca"; + StateDirectoryMode = "0755"; - # Avoid race conditions creating the CA for selfsigned certs - (lib.mkIf cfg.preliminarySelfsigned { - path = [ pkgs.minica ]; - # Working directory will be /tmp - script = '' - test -e ca/key.pem || minica \ - --ca-key ca/key.pem \ - --ca-cert ca/cert.pem \ - --domains selfsigned.local - ''; - serviceConfig = { - StateDirectory = [ "acme/.minica" ]; - BindPaths = "/var/lib/acme/.minica:/tmp/ca"; - }; - }) - ]; + # Creates ${lockdir}. Earlier RemainAfterExit=true means + # it does not get deleted immediately. + RuntimeDirectory = "acme"; + RuntimeDirectoryMode = "0700"; + + # Generally, we don't write anything that should be group accessible. + # Group varies for most ACME units, and setup files are only used + # under the acme user. + UMask = "0077"; + }; + }; certToConfig = cert: data: @@ -219,7 +210,6 @@ let acmeServer = data.server; useDns = data.dnsProvider != null; destPath = "/var/lib/acme/${cert}"; - selfsignedDeps = lib.optionals (cfg.preliminarySelfsigned) [ "acme-selfsigned-${cert}.service" ]; # Minica and lego have a "feature" which replaces * with _. We need # to make this substitution to reference the output files from both programs. @@ -339,16 +329,18 @@ let certificateKey = if data.csrKey != null then "${data.csrKey}" else "certificates/${keyName}.key"; in { - inherit accountHash cert selfsignedDeps; + inherit accountHash cert; group = data.group; renewTimer = { description = "Renew ACME Certificate for ${cert}"; wantedBy = [ "timers.target" ]; + # Avoid triggering certificate renewals accidentally when running s-t-c. + unitConfig."X-OnlyManualStart" = true; timerConfig = { OnCalendar = data.renewInterval; - Unit = "acme-${cert}.service"; + Unit = "acme-order-renew-${cert}.service"; Persistent = "yes"; # Allow systemd to pick a convenient time within the day @@ -364,15 +356,29 @@ let }; }; - selfsignService = lockfileName: { - description = "Generate self-signed certificate for ${cert}"; + baseService = lockfileName: { + description = "Ensure certificate for ${cert}"; + + wantedBy = [ "multi-user.target" ]; + after = [ "acme-setup.service" ]; - requires = [ "acme-setup.service" ]; + + # Whenever this service starts (on boot, through dependencies, through + # changes) we trigger the acme-order-renew service to give it a chance + # to catch up with the potentially changed config. + wants = [ + "acme-setup.service" + "acme-order-renew-${cert}.service" + ]; + before = [ "acme-order-renew-${cert}.service" ]; + + restartTriggers = [ + config.systemd.services."acme-order-renew-${cert}".script + ]; path = [ pkgs.minica ]; unitConfig = { - ConditionPathExists = "!/var/lib/acme/${cert}/key.pem"; StartLimitIntervalSec = 0; }; @@ -380,11 +386,13 @@ let Group = data.group; UMask = "0027"; + RemainAfterExit = true; + StateDirectory = "acme/${cert}"; BindPaths = [ "/var/lib/acme/.minica:/tmp/ca" - "/var/lib/acme/${cert}:/tmp/${keyName}" + "/var/lib/acme/${cert}:/tmp/out" ]; }; @@ -392,40 +400,69 @@ let # minica will output to a folder sharing the name of the first domain # in the list, which will be ${data.domain} script = (if (lockfileName == null) then lib.id else wrapInFlock "${lockdir}${lockfileName}") '' + set -ex + + # Regenerate self-signed certificates (in case the SANs change) until we + # have seen a succesfull ACME certificate at least once. + if [ -e out/acme-success ]; then + exit 0 + fi + minica \ --ca-key ca/key.pem \ --ca-cert ca/cert.pem \ --domains ${lib.escapeShellArg (builtins.concatStringsSep "," ([ data.domain ] ++ extraDomains))} # Create files to match directory layout for real certificates - cd '${keyName}' - cp ../ca/cert.pem chain.pem - cat cert.pem chain.pem > fullchain.pem - cat key.pem fullchain.pem > full.pem + ( + cd '${keyName}' + cp -vp cert.pem ../out/cert.pem + cp -vp key.pem ../out/key.pem + ) + cat out/cert.pem ca/cert.pem > out/fullchain.pem + cp ca/cert.pem out/chain.pem + cat out/key.pem out/fullchain.pem > out/full.pem - # Group might change between runs, re-apply it - chown '${user}:${data.group}' -- * + # Fix up the output files to adhere to the group and + # have consistent permissions. This needs to be kept + # consistent with the acme-setup script above. + for fixpath in out certificates; do + if [ -d "$fixpath" ]; then + chmod -R u=rwX,g=rX,o= "$fixpath" + chown -R ${user}:${data.group} "$fixpath" + fi + done - # Default permissions make the files unreadable by group + anon - # Need to be readable by group - chmod 640 -- * + ${lib.optionalString (data.webroot != null) '' + # Ensure the webroot exists. Fixing group is required in case configuration was changed between runs. + # Lego will fail if the webroot does not exist at all. + ( + mkdir -p '${data.webroot}/.well-known/acme-challenge' \ + && chgrp '${data.group}' ${data.webroot}/.well-known/acme-challenge + ) || ( + echo 'Please ensure ${data.webroot}/.well-known/acme-challenge exists and is writable by acme:${data.group}' \ + && exit 1 + ) + ''} ''; }; - renewService = lockfileName: { - description = "Renew ACME certificate for ${cert}"; + orderRenewService = lockfileName: { + description = "Order (and renew) ACME certificate for ${cert}"; after = [ "network.target" "network-online.target" "acme-setup.service" "nss-lookup.target" - ] - ++ selfsignedDeps; - wants = [ "network-online.target" ] ++ selfsignedDeps; - requires = [ "acme-setup.service" ]; - - # https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099 - wantedBy = lib.optionals (!config.boot.isContainer) [ "multi-user.target" ]; + "acme-${cert}.service" + ]; + wants = [ + "network-online.target" + "acme-setup.service" + "acme-${cert}.service" + ]; + # Ensure that certificates are generated if people use `security.acme.certs` + # without having/declaring other systemd units that depend on the cert. path = with pkgs; [ lego @@ -523,25 +560,12 @@ let [[ $expiration_days -gt ${toString data.validMinDays} ]] } - ${lib.optionalString (data.webroot != null) '' - # Ensure the webroot exists. Fixing group is required in case configuration was changed between runs. - # Lego will fail if the webroot does not exist at all. - ( - mkdir -p '${data.webroot}/.well-known/acme-challenge' \ - && chgrp '${data.group}' ${data.webroot}/.well-known/acme-challenge - ) || ( - echo 'Please ensure ${data.webroot}/.well-known/acme-challenge exists and is writable by acme:${data.group}' \ - && exit 1 - ) - ''} - echo '${domainHash}' > domainhash.txt - # Check if we can renew. + # Check if a new order is needed # We can only renew if the list of domains has not changed. # We also need an account key. Avoids #190493 if cmp -s domainhash.txt certificates/domainhash.txt && [ -e '${certificateKey}' ] && [ -e 'certificates/${keyName}.crt' ] && [ -n "$(find accounts -name '${data.email}.key')" ]; then - # Even if a cert is not expired, it may be revoked by the CA. # Try to renew, and silently fail if the cert is not expired. # Avoids #85794 and resolves #129838 @@ -553,13 +577,12 @@ let exit 11 fi fi - - # Otherwise do a full run + # Do a full run elif ! lego ${runOpts}; then # Produce a nice error for those doing their first nixos-rebuild with these certs echo Failed to fetch certificates. \ This may mean your DNS records are set up incorrectly. \ - ${lib.optionalString (cfg.preliminarySelfsigned) "Selfsigned certs are in place and dependant services will still start."} + Self-signed certs are in place and dependant services will still start. # Exit 10 so that users can potentially amend SuccessExitStatus to ignore this error. # High number to avoid Systemd reserved codes. exit 10 @@ -567,10 +590,12 @@ let mv domainhash.txt certificates/ - # Group might change between runs, re-apply it - chown '${user}:${data.group}' certificates/* + touch out/acme-success # Copy all certs to the "real" certs directory + # lego has only an interesting subset of files available, + # construct reasonably compatible files that clients can consume + # as expected. if ! cmp -s 'certificates/${keyName}.crt' out/fullchain.pem; then touch out/renewed echo Installing new certificate @@ -581,10 +606,13 @@ let cat out/key.pem out/fullchain.pem > out/full.pem fi - # By default group will have no access to the cert files. - # This chmod will fix that. - chmod 640 out/* - + # Keep permissions consistent. Needs to be in sync with the other scripts. + for fixpath in out certificates; do + if [ -d "$fixpath" ]; then + chmod -R u=rwX,g=rX,o= "$fixpath" + chown -R ${user}:${data.group} "$fixpath" + fi + done # Also ensure safer permissions on the account directory. chmod -R u=rwX,g=,o= accounts/. ''; @@ -905,19 +933,6 @@ in options = { security.acme = { - preliminarySelfsigned = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Whether a preliminary self-signed certificate should be generated before - doing ACME requests. This can be useful when certificates are required in - a webserver, but ACME needs the webserver to make its requests. - - With preliminary self-signed certificate the webserver can be started and - can later reload the correct ACME certificates. - ''; - }; - acceptTerms = lib.mkOption { type = lib.types.bool; default = false; @@ -1003,10 +1018,13 @@ in "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." ) (lib.mkRemovedOptionModule [ "security" "acme" "preDelay" ] - "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal" + "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service and Before=acme-\${cert}.service to the service you want to execute before the cert renewal" ) (lib.mkRemovedOptionModule [ "security" "acme" "activationDelay" ] - "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal" + "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service and Before=acme-\${cert}.service to the service you want to execute before the cert renewal" + ) + (lib.mkRemovedOptionModule [ "security" "acme" "preliminarySelfsigned" ] + "This option has been removed. Preliminary self-signed certificates are now always generated to simplify the dependency structure." ) (lib.mkChangedOptionModule [ "security" "acme" "validMin" ] @@ -1161,45 +1179,35 @@ in systemd.services = let - renewServiceFunctions = lib.mapAttrs' ( - cert: conf: lib.nameValuePair "acme-${cert}" conf.renewService + orderRenewServiceFunctions = lib.mapAttrs' ( + cert: conf: lib.nameValuePair "acme-order-renew-${cert}" conf.orderRenewService ) certConfigs; - renewServices = + orderRenewServices = if cfg.maxConcurrentRenewals > 0 then - roundRobinApplyAttrs renewServiceFunctions concurrencyLockfiles + roundRobinApplyAttrs orderRenewServiceFunctions concurrencyLockfiles else - lib.mapAttrs (_: f: f null) renewServiceFunctions; - selfsignServiceFunctions = lib.mapAttrs' ( - cert: conf: lib.nameValuePair "acme-selfsigned-${cert}" conf.selfsignService + lib.mapAttrs (_: f: f null) orderRenewServiceFunctions; + baseServiceFunctions = lib.mapAttrs' ( + cert: conf: lib.nameValuePair "acme-${cert}" conf.baseService ) certConfigs; - selfsignServices = + baseServices = if cfg.maxConcurrentRenewals > 0 then - roundRobinApplyAttrs selfsignServiceFunctions concurrencyLockfiles + roundRobinApplyAttrs baseServiceFunctions concurrencyLockfiles else - lib.mapAttrs (_: f: f null) selfsignServiceFunctions; + lib.mapAttrs (_: f: f null) baseServiceFunctions; in { acme-setup = setupService; } - // renewServices - // lib.optionalAttrs cfg.preliminarySelfsigned selfsignServices; + // baseServices + // orderRenewServices; systemd.timers = lib.mapAttrs' ( - cert: conf: lib.nameValuePair "acme-${cert}" conf.renewTimer + cert: conf: lib.nameValuePair "acme-renew-${cert}" conf.renewTimer ) certConfigs; systemd.targets = let - # Create some targets which can be depended on to be "active" after cert renewals - finishedTargets = lib.mapAttrs' ( - cert: conf: - lib.nameValuePair "acme-finished-${cert}" { - wantedBy = [ "default.target" ]; - requires = [ "acme-${cert}.service" ]; - after = [ "acme-${cert}.service" ]; - } - ) certConfigs; - # Create targets to limit the number of simultaneous account creations # How it works: # - Pick a "leader" cert service, which will be in charge of creating the account, @@ -1214,8 +1222,8 @@ in let dnsConfs = builtins.filter (conf: cfg.certs.${conf.cert}.dnsProvider != null) confs; leaderConf = if dnsConfs != [ ] then builtins.head dnsConfs else builtins.head confs; - leader = "acme-${leaderConf.cert}.service"; - followers = map (conf: "acme-${conf.cert}.service") ( + leader = "acme-order-renew-${leaderConf.cert}.service"; + followers = map (conf: "acme-order-renew-${conf.cert}.service") ( builtins.filter (conf: conf != leaderConf) confs ); in @@ -1224,10 +1232,11 @@ in before = followers; requires = [ leader ]; after = [ leader ]; + unitConfig.RefuseManualStart = true; } ) (lib.groupBy (conf: conf.accountHash) (lib.attrValues certConfigs)); in - finishedTargets // accountTargets; + accountTargets; }) ]; diff --git a/nixos/modules/services/networking/doh-server.nix b/nixos/modules/services/networking/doh-server.nix index 834179ef9001..ab81e299390d 100644 --- a/nixos/modules/services/networking/doh-server.nix +++ b/nixos/modules/services/networking/doh-server.nix @@ -156,7 +156,7 @@ in "network.target" ] ++ lib.optional (cfg.useACMEHost != null) "acme-${cfg.useACMEHost}.service"; - wants = lib.optional (cfg.useACMEHost != null) "acme-finished-${cfg.useACMEHost}.target"; + wants = lib.optional (cfg.useACMEHost != null) "acme-${cfg.useACMEHost}.service"; wantedBy = [ "multi-user.target" ]; serviceConfig = { AmbientCapabilities = "CAP_NET_BIND_SERVICE"; diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix index aa3ae96896d6..07d4ad7fccfa 100644 --- a/nixos/modules/services/web-servers/apache-httpd/default.nix +++ b/nixos/modules/services/web-servers/apache-httpd/default.nix @@ -48,8 +48,6 @@ let ) (filter (hostOpts: hostOpts.enableACME || hostOpts.useACMEHost != null) vhosts); vhostCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts); - dependentCertNames = filter (cert: certs.${cert}.dnsProvider == null) vhostCertNames; # those that might depend on the HTTP server - independentCertNames = filter (cert: certs.${cert}.dnsProvider != null) vhostCertNames; # those that don't depend on the HTTP server mkListenInfo = hostOpts: @@ -914,13 +912,14 @@ in systemd.services.httpd = { description = "Apache HTTPD"; wantedBy = [ "multi-user.target" ]; - wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) vhostCertNames); + wants = concatLists (map (certName: [ "acme-${certName}.service" ]) vhostCertNames); after = [ "network.target" ] - ++ map (certName: "acme-selfsigned-${certName}.service") vhostCertNames - ++ map (certName: "acme-${certName}.service") independentCertNames; # avoid loading self-signed key w/ real cert, or vice-versa - before = map (certName: "acme-${certName}.service") dependentCertNames; + # Ensure httpd runs with baseline certificates in place. + ++ map (certName: "acme-${certName}.service") vhostCertNames; + # Ensure httpd runs (with current config) before the actual ACME jobs run + before = map (certName: "acme-order-renew-${certName}.service") vhostCertNames; restartTriggers = [ cfg.configFile ]; path = [ @@ -960,19 +959,17 @@ in # postRun hooks on cert renew can't be used to restart Apache since renewal # runs as the unprivileged acme user. sslTargets are added to wantedBy + before - # which allows the acme-finished-$cert.target to signify the successful updating + # which allows the acme-order-renew-$cert.service to signify the successful updating # of certs end-to-end. systemd.services.httpd-config-reload = let - sslServices = map (certName: "acme-${certName}.service") vhostCertNames; - sslTargets = map (certName: "acme-finished-${certName}.target") vhostCertNames; + sslServices = map (certName: "acme-order-renew-${certName}.service") vhostCertNames; in mkIf (vhostCertNames != [ ]) { wantedBy = sslServices ++ [ "multi-user.target" ]; # Before the finished targets, after the renew services. # This service might be needed for HTTP-01 challenges, but we only want to confirm # certs are updated _after_ config has been reloaded. - before = sslTargets; after = sslServices; restartTriggers = [ cfg.configFile ]; # Block reloading if not all certs exist yet. diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix index 73da17d5a050..2277105069f8 100644 --- a/nixos/modules/services/web-servers/caddy/default.nix +++ b/nixos/modules/services/web-servers/caddy/default.nix @@ -14,13 +14,11 @@ let virtualHosts = attrValues cfg.virtualHosts; acmeEnabledVhosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts; vhostCertNames = unique (map (hostOpts: hostOpts.useACMEHost) acmeEnabledVhosts); - dependentCertNames = filter (cert: certs.${cert}.dnsProvider == null) vhostCertNames; # those that might depend on the HTTP server - independentCertNames = filter (cert: certs.${cert}.dnsProvider != null) vhostCertNames; # those that don't depend on the HTTP server mkVHostConf = hostOpts: let - sslCertDir = config.security.acme.certs.${hostOpts.useACMEHost}.directory; + sslCertDir = certs.${hostOpts.useACMEHost}.directory; in '' ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} { @@ -392,7 +390,7 @@ in ++ map ( name: mkCertOwnershipAssertion { - cert = config.security.acme.certs.${name}; + cert = certs.${name}; groups = config.users.groups; services = [ config.systemd.services.caddy ]; } @@ -412,11 +410,8 @@ in systemd.packages = [ cfg.package ]; systemd.services.caddy = { - wants = map (certName: "acme-finished-${certName}.target") vhostCertNames; - after = - map (certName: "acme-selfsigned-${certName}.service") vhostCertNames - ++ map (certName: "acme-${certName}.service") independentCertNames; # avoid loading self-signed key w/ real cert, or vice-versa - before = map (certName: "acme-${certName}.service") dependentCertNames; + wants = map (certName: "acme-${certName}.service") vhostCertNames; + after = map (certName: "acme-${certName}.service") vhostCertNames; wantedBy = [ "multi-user.target" ]; startLimitIntervalSec = 14400; diff --git a/nixos/modules/services/web-servers/h2o/default.nix b/nixos/modules/services/web-servers/h2o/default.nix index bd3b615ec9c8..331c31fefc9b 100644 --- a/nixos/modules/services/web-servers/h2o/default.nix +++ b/nixos/modules/services/web-servers/h2o/default.nix @@ -434,14 +434,13 @@ in systemd.services.h2o = { description = "H2O HTTP server"; wantedBy = [ "multi-user.target" ]; - wants = lib.concatLists (map (certName: [ "acme-finished-${certName}.target" ]) acmeCertNames.all); + wants = lib.concatLists (map (certName: [ "acme-${certName}.service" ]) acmeCertNames.all); # Since H2O will be hosting the challenges, H2O must be started - before = builtins.map (certName: "acme-${certName}.service") acmeCertNames.dependent; + before = builtins.map (certName: "acme-order-renew-${certName}.service") acmeCertNames.all; after = [ "network.target" ] - ++ builtins.map (certName: "acme-selfsigned-${certName}.service") acmeCertNames.all - ++ builtins.map (certName: "acme-${certName}.service") acmeCertNames.independent; # avoid loading self-signed key w/ real cert, or vice-versa + ++ builtins.map (certName: "acme-${certName}.service") acmeCertNames.all; serviceConfig = { ExecStart = "${h2oExe} --mode 'master'"; @@ -490,16 +489,14 @@ in # This service waits for all certificates to be available before reloading # H2O configuration. `tlsTargets` are added to `wantedBy` + `before` which - # allows the `acme-finished-$cert.target` to signify the successful updating + # allows the `acme-order-renew-$cert.service` to signify the successful updating # of certs end-to-end. systemd.services.h2o-config-reload = let - tlsTargets = map (certName: "acme-${certName}.target") acmeCertNames.all; - tlsServices = map (certName: "acme-${certName}.service") acmeCertNames.all; + tlsServices = map (certName: "acme-order-renew-${certName}.service") acmeCertNames.all; in mkIf (acmeCertNames.all != [ ]) { wantedBy = tlsServices ++ [ "multi-user.target" ]; - before = tlsTargets; after = tlsServices; unitConfig = { ConditionPathExists = map ( diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix index 553f1f1148a5..267c0abcd44e 100644 --- a/nixos/modules/services/web-servers/nginx/default.nix +++ b/nixos/modules/services/web-servers/nginx/default.nix @@ -15,8 +15,6 @@ let vhostConfig: vhostConfig.enableACME || vhostConfig.useACMEHost != null ) vhostsConfigs; vhostCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts); - dependentCertNames = filter (cert: certs.${cert}.dnsProvider == null) vhostCertNames; # those that might depend on the HTTP server - independentCertNames = filter (cert: certs.${cert}.dnsProvider != null) vhostCertNames; # those that don't depend on the HTTP server virtualHosts = mapAttrs ( vhostName: vhostConfig: let @@ -442,6 +440,7 @@ let auth_basic off; auth_request off; proxy_pass http://${vhost.acmeFallbackHost}; + proxy_set_header Host $host; } ''} ''; @@ -1481,16 +1480,14 @@ in systemd.services.nginx = { description = "Nginx Web Server"; wantedBy = [ "multi-user.target" ]; - wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) vhostCertNames); + wants = concatLists (map (certName: [ "acme-${certName}.service" ]) vhostCertNames); after = [ "network.target" ] - ++ map (certName: "acme-selfsigned-${certName}.service") vhostCertNames - ++ map (certName: "acme-${certName}.service") independentCertNames; # avoid loading self-signed key w/ real cert, or vice-versa - # Nginx needs to be started in order to be able to request certificates - # (it's hosting the acme challenge after all) - # This fixes https://github.com/NixOS/nixpkgs/issues/81842 - before = map (certName: "acme-${certName}.service") dependentCertNames; + # Ensure nginx runs with baseline certificates in place. + ++ map (certName: "acme-${certName}.service") vhostCertNames; + # Ensure nginx runs (with current config) before the actual ACME jobs run + before = map (certName: "acme-order-renew-${certName}.service") vhostCertNames; stopIfChanged = false; preStart = '' ${cfg.preStart} @@ -1585,26 +1582,24 @@ in # This service waits for all certificates to be available # before reloading nginx configuration. # sslTargets are added to wantedBy + before - # which allows the acme-finished-$cert.target to signify the successful updating + # which allows the acme-order-renew-$cert.service to signify the successful updating # of certs end-to-end. systemd.services.nginx-config-reload = let - sslServices = map (certName: "acme-${certName}.service") vhostCertNames; - sslTargets = map (certName: "acme-finished-${certName}.target") vhostCertNames; + sslOrderRenewServices = map (certName: "acme-order-renew-${certName}.service") vhostCertNames; in mkIf (cfg.enableReload || vhostCertNames != [ ]) { wants = optionals cfg.enableReload [ "nginx.service" ]; - wantedBy = sslServices ++ [ "multi-user.target" ]; - # Before the finished targets, after the renew services. + wantedBy = sslOrderRenewServices ++ [ "multi-user.target" ]; + # XXX Before the finished targets, after the renew services. # This service might be needed for HTTP-01 challenges, but we only want to confirm # certs are updated _after_ config has been reloaded. - before = sslTargets; - after = sslServices; + after = sslOrderRenewServices; restartTriggers = optionals cfg.enableReload [ configFile ]; # Block reloading if not all certs exist yet. # Happens when config changes add new vhosts/certs. unitConfig = { - ConditionPathExists = optionals (sslServices != [ ]) ( + ConditionPathExists = optionals (vhostCertNames != [ ]) ( map (certName: certs.${certName}.directory + "/fullchain.pem") vhostCertNames ); # Disable rate limiting for this, because it may be triggered quickly a bunch of times diff --git a/nixos/modules/services/web-servers/pomerium.nix b/nixos/modules/services/web-servers/pomerium.nix index 93b04425435d..e4e9bc2e17a8 100644 --- a/nixos/modules/services/web-servers/pomerium.nix +++ b/nixos/modules/services/web-servers/pomerium.nix @@ -72,11 +72,11 @@ in wants = [ "network.target" ] - ++ (optional (cfg.useACMEHost != null) "acme-finished-${cfg.useACMEHost}.target"); + ++ (optional (cfg.useACMEHost != null) "acme-${cfg.useACMEHost}.service"); after = [ "network.target" ] - ++ (optional (cfg.useACMEHost != null) "acme-finished-${cfg.useACMEHost}.target"); + ++ (optional (cfg.useACMEHost != null) "acme-${cfg.useACMEHost}.service"); wantedBy = [ "multi-user.target" ]; environment = optionalAttrs (cfg.useACMEHost != null) { CERTIFICATE_FILE = "fullchain.pem"; @@ -127,18 +127,16 @@ in # postRun hooks on cert renew can't be used to restart Nginx since renewal # runs as the unprivileged acme user. sslTargets are added to wantedBy + before - # which allows the acme-finished-$cert.target to signify the successful updating + # which allows the acme-order-renew-$cert.target to signify the successful updating # of certs end-to-end. systemd.services.pomerium-config-reload = mkIf (cfg.useACMEHost != null) { # TODO(lukegb): figure out how to make config reloading work with credentials. wantedBy = [ - "acme-finished-${cfg.useACMEHost}.target" + "acme-order-renew-${cfg.useACMEHost}.service" "multi-user.target" ]; - # Before the finished targets, after the renew services. - before = [ "acme-finished-${cfg.useACMEHost}.target" ]; - after = [ "acme-${cfg.useACMEHost}.service" ]; + after = [ "acme-order-renew-${cfg.useACMEHost}.service" ]; # Block reloading if not all certs exist yet. unitConfig.ConditionPathExists = [ "${config.security.acme.certs.${cfg.useACMEHost}.directory}/fullchain.pem" diff --git a/nixos/tests/acme/caddy.nix b/nixos/tests/acme/caddy.nix index 96c86086d50a..0a3af2614bdf 100644 --- a/nixos/tests/acme/caddy.nix +++ b/nixos/tests/acme/caddy.nix @@ -85,33 +85,24 @@ in ca_domain = "${nodes.acme.test-support.acme.caDomain}" fqdn = "${nodes.caddy.networking.fqdn}" + with subtest("Boot and start with selfsigned certificates"): + caddy.start() + caddy.wait_for_unit("caddy.service") + check_issuer(caddy, fqdn, "minica") + # Check that the web server has picked up the selfsigned cert + check_connection(caddy, fqdn, minica=True) + acme.start() wait_for_running(acme) acme.wait_for_open_port(443) - with subtest("Boot and acquire a new cert"): - caddy.start() - wait_for_running(caddy) - + with subtest("Acquire a new cert"): + caddy.succeed(f"systemctl restart acme-{fqdn}.service") check_issuer(caddy, fqdn, "pebble") check_domain(caddy, fqdn, fqdn) - download_ca_certs(caddy, ca_domain) check_connection(caddy, fqdn) - with subtest("Can run on selfsigned certificates"): - # Switch to selfsigned first - caddy.succeed(f"systemctl clean acme-{fqdn}.service --what=state") - caddy.succeed(f"systemctl start acme-selfsigned-{fqdn}.service") - check_issuer(caddy, fqdn, "minica") - caddy.succeed("systemctl restart caddy.service") - # Check that the web server has picked up the selfsigned cert - check_connection(caddy, fqdn, minica=True) - caddy.succeed(f"systemctl start acme-{fqdn}.service") - # This may fail a couple of times before caddy is restarted - check_issuer(caddy, fqdn, "pebble") - check_connection(caddy, fqdn) - with subtest("security.acme changes reflect on caddy"): check_connection(caddy, f"caddy-alt.{domain}", fail=True) switch_to(caddy, "add_domain") diff --git a/nixos/tests/acme/default.nix b/nixos/tests/acme/default.nix index 7f861be2a027..2faca3a0b842 100644 --- a/nixos/tests/acme/default.nix +++ b/nixos/tests/acme/default.nix @@ -1,10 +1,14 @@ { runTest }: +let + domain = "example.test"; +in { http01-builtin = runTest ./http01-builtin.nix; dns01 = runTest ./dns01.nix; caddy = runTest ./caddy.nix; nginx = runTest ( import ./webserver.nix { + inherit domain; serverName = "nginx"; group = "nginx"; baseModule = { @@ -22,17 +26,17 @@ addSSL = true; useACMEHost = "proxied.example.test"; acmeFallbackHost = "localhost:8080"; - # lego will refuse the request if the host header is not correct - extraConfig = '' - proxy_set_header Host $host; - ''; }; }; + specialisation.nullroot.configuration = { + services.nginx.virtualHosts."nullroot.${domain}".acmeFallbackHost = "localhost:8081"; + }; }; } ); httpd = runTest ( import ./webserver.nix { + inherit domain; serverName = "httpd"; group = "wwwrun"; baseModule = { @@ -50,6 +54,16 @@ }; }; }; + specialisation.nullroot.configuration = { + services.httpd.virtualHosts."nullroot.${domain}" = { + locations."/.well-known/acme-challenge" = { + proxyPass = "http://localhost:8081/.well-known/acme-challenge"; + extraConfig = '' + ProxyPreserveHost On + ''; + }; + }; + }; }; } ); diff --git a/nixos/tests/acme/http01-builtin.nix b/nixos/tests/acme/http01-builtin.nix index 8589190a2667..d92737ed6fb1 100644 --- a/nixos/tests/acme/http01-builtin.nix +++ b/nixos/tests/acme/http01-builtin.nix @@ -37,6 +37,12 @@ in listenHTTP = ":80"; }; + systemd.targets."renew-triggered" = { + wantedBy = [ "acme-order-renew-${config.networking.fqdn}.service" ]; + after = [ "acme-order-renew-${config.networking.fqdn}.service" ]; + unitConfig.RefuseManualStart = true; + }; + specialisation = { renew.configuration = { # Pebble provides 5 year long certs, @@ -177,17 +183,29 @@ in # old_hash will be used in the preservation tests later old_hash = hash builtin.succeed(f"systemctl start acme-{cert}.service") + builtin.succeed(f"systemctl start acme-order-renew-{cert}.service") + builtin.wait_for_unit("renew-triggered.target") + hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem") assert hash == hash_after, "Certificate was unexpectedly changed" + builtin.succeed("systemctl stop renew-triggered.target") switch_to(builtin, "renew") + builtin.wait_for_unit("renew-triggered.target") + check_issuer(builtin, cert, "pebble") hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem | tee /dev/stderr") assert hash != hash_after, "Certificate was not renewed" + check_permissions(builtin, cert, "acme") + with subtest("Handles email change correctly"): hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem") + + builtin.succeed("systemctl stop renew-triggered.target") switch_to(builtin, "accountchange") + builtin.wait_for_unit("renew-triggered.target") + check_issuer(builtin, cert, "pebble") # Check that there are now 2 account directories builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2") @@ -202,58 +220,101 @@ in # old_hash will be used in the preservation tests later old_hash = hash_after + check_permissions(builtin, cert, "acme") + with subtest("Correctly implements OCSP stapling"): check_stapling(builtin, cert, "${caDomain}", fail=True) + + builtin.succeed("systemctl stop renew-triggered.target") switch_to(builtin, "ocsp_stapling") + builtin.wait_for_unit("renew-triggered.target") + check_stapling(builtin, cert, "${caDomain}") + check_permissions(builtin, cert, "acme") with subtest("Handles keyType change correctly"): check_key_bits(builtin, cert, 256) + + builtin.succeed("systemctl stop renew-triggered.target") switch_to(builtin, "keytype") + builtin.wait_for_unit("renew-triggered.target") + check_key_bits(builtin, cert, 384) # keyType is part of the accountHash, thus a new account will be created builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2") + check_permissions(builtin, cert, "acme") with subtest("Reuses generated, valid certs from previous configurations"): # Right now, the hash should not match due to the previous test hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem | tee /dev/stderr") assert hash != old_hash, "Expected certificate to differ" + + builtin.succeed("systemctl stop renew-triggered.target") switch_to(builtin, "preservation") + builtin.wait_for_unit("renew-triggered.target") + hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem | tee /dev/stderr") assert hash == old_hash, "Expected certificate to match from older configuration" + check_permissions(builtin, cert, "acme") with subtest("Add a new cert, extend existing cert domains"): check_domain(builtin, cert, f"builtin-alt.{domain}", fail=True) + + builtin.succeed("systemctl stop renew-triggered.target") switch_to(builtin, "add_cert_and_domain") + builtin.wait_for_unit("renew-triggered.target") + check_issuer(builtin, cert, "pebble") check_domain(builtin, cert, f"builtin-alt.{domain}") check_issuer(builtin, cert2, "pebble") check_domain(builtin, cert2, cert2) # There should not be a new account folder created builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2") + check_permissions(builtin, cert, "acme") + check_permissions(builtin, cert2, "acme") with subtest("Check account hashing compatibility with pre-24.05 settings"): - switch_to(builtin, "legacy_account_hash", fail=True) - builtin.succeed(f"stat {legacy_account_dir} > /dev/stderr && rm -rf {legacy_account_dir}") + builtin.succeed("systemctl stop renew-triggered.target") + switch_to(builtin, "legacy_account_hash" + ) + builtin.wait_for_unit("renew-triggered.target") - with subtest("Ensure Concurrency limits work"): + builtin.succeed(f"stat {legacy_account_dir} > /dev/stderr && rm -rf {legacy_account_dir}") + check_permissions(builtin, cert, "acme") + + with subtest("Ensure concurrency limits work"): + builtin.succeed("systemctl stop renew-triggered.target") switch_to(builtin, "concurrency") + builtin.wait_for_unit("renew-triggered.target") + check_issuer(builtin, cert3, "pebble") check_domain(builtin, cert3, cert3) + check_permissions(builtin, cert, "acme") + + with subtest("Can renew using a CSR"): + builtin.succeed(f"systemctl stop acme-{cert}.service") + builtin.succeed(f"systemctl clean acme-{cert}.service --what=state") + + builtin.succeed("systemctl stop renew-triggered.target") + switch_to(builtin, "csr") + builtin.wait_for_unit("renew-triggered.target") + + check_issuer(builtin, cert, "pebble") with subtest("Generate self-signed certs"): + acme.shutdown() + check_issuer(builtin, cert, "pebble") + + builtin.succeed(f"systemctl stop acme-{cert}.service") builtin.succeed(f"systemctl clean acme-{cert}.service --what=state") - builtin.succeed(f"systemctl start acme-selfsigned-{cert}.service") + builtin.succeed(f"systemctl start acme-{cert}.service") + check_issuer(builtin, cert, "minica") check_domain(builtin, cert, cert) with subtest("Validate permissions (self-signed)"): check_permissions(builtin, cert, "acme") - with subtest("Can renew using a CSR"): - builtin.succeed(f"systemctl clean acme-{cert}.service --what=state") - switch_to(builtin, "csr") - check_issuer(builtin, cert, "pebble") ''; } diff --git a/nixos/tests/acme/python-utils.py b/nixos/tests/acme/python-utils.py index 77e7066e0349..d542324084aa 100644 --- a/nixos/tests/acme/python-utils.py +++ b/nixos/tests/acme/python-utils.py @@ -3,6 +3,36 @@ import time TOTAL_RETRIES = 20 +# BackoffTracker provides a robust system for handling test retries +class BackoffTracker: + delay = 1 + increment = 1 + + def handle_fail(self, retries, message) -> int: + assert retries < TOTAL_RETRIES, message + + print(f"Retrying in {self.delay}s, {retries + 1}/{TOTAL_RETRIES}") + time.sleep(self.delay) + + # Only increment after the first try + if retries == 0: + self.delay += self.increment + self.increment *= 2 + + return retries + 1 + + def protect(self, func): + def wrapper(*args, retries: int = 0, **kwargs): + try: + return func(*args, **kwargs) + except Exception as err: + retries = self.handle_fail(retries, err.args) + return wrapper(*args, retries=retries, **kwargs) + + return wrapper + + +backoff = BackoffTracker() def run(node, cmd, fail=False): if fail: @@ -39,6 +69,7 @@ def switch_to(node, name, fail=False) -> None: # and matches the issuer we expect it to be. # It's a good validation to ensure the cert.pem and fullchain.pem # are not still selfsigned after verification +@backoff.protect def check_issuer(node, cert_name, issuer) -> None: for fname in ("cert.pem", "fullchain.pem"): actual_issuer = node.succeed( @@ -102,9 +133,10 @@ def check_permissions(node, cert_name, group): f"test $({stat} /var/lib/acme/{cert_name}/*.pem" f" | tee /dev/stderr | grep -v '640 acme {group}' | wc -l) -eq 0" ) + node.execute(f"ls -lahR /var/lib/acme/.lego/{cert_name}/* > /dev/stderr") node.succeed( f"test $({stat} /var/lib/acme/.lego/{cert_name}/*/{cert_name}*" - f" | tee /dev/stderr | grep -v '600 acme {group}' | wc -l) -eq 0" + f" | tee /dev/stderr | grep -v '640 acme {group}' | wc -l) -eq 0" ) node.succeed( f"test $({stat} /var/lib/acme/{cert_name}" @@ -115,37 +147,6 @@ def check_permissions(node, cert_name, group): f" | tee /dev/stderr | grep -v '600 acme {group}' | wc -l) -eq 0" ) -# BackoffTracker provides a robust system for handling test retries -class BackoffTracker: - delay = 1 - increment = 1 - - def handle_fail(self, retries, message) -> int: - assert retries < TOTAL_RETRIES, message - - print(f"Retrying in {self.delay}s, {retries + 1}/{TOTAL_RETRIES}") - time.sleep(self.delay) - - # Only increment after the first try - if retries == 0: - self.delay += self.increment - self.increment *= 2 - - return retries + 1 - - def protect(self, func): - def wrapper(*args, retries: int = 0, **kwargs): - try: - return func(*args, **kwargs) - except Exception as err: - retries = self.handle_fail(retries, err.args) - return wrapper(*args, retries=retries, **kwargs) - - return wrapper - - -backoff = BackoffTracker() - @backoff.protect def download_ca_certs(node, ca_domain): diff --git a/nixos/tests/acme/webserver.nix b/nixos/tests/acme/webserver.nix index a19f82edc76f..5dcd8857e8d2 100644 --- a/nixos/tests/acme/webserver.nix +++ b/nixos/tests/acme/webserver.nix @@ -2,7 +2,7 @@ serverName, group, baseModule, - domain ? "example.test", + domain, }: { config, @@ -18,6 +18,8 @@ timeout = 300; }; + interactive.sshBackdoor.enable = true; + nodes = { # The fake ACME server which will respond to client requests acme = @@ -45,6 +47,7 @@ "certchange.${domain}" "zeroconf.${domain}" "zeroconf2.${domain}" + "zeroconf3.${domain}" "nullroot.${domain}" ]; @@ -57,6 +60,7 @@ systemd.targets."renew-triggered" = { wantedBy = [ "${serverName}-config-reload.service" ]; after = [ "${serverName}-config-reload.service" ]; + unitConfig.RefuseManualStart = true; }; security.acme.certs."proxied.${domain}" = { @@ -101,13 +105,42 @@ # Test that "acmeRoot = null" still results in # valid cert generation by inheriting defaults. nullroot.configuration = { - security.acme.defaults.listenHTTP = ":8080"; + # The default.nix has the server-type dependent config statements + # to properly set up the proxying. We need a separate port here to + # avoid hostname issues with the proxy already running on :8080 + security.acme.defaults.listenHTTP = ":8081"; services.${serverName}.virtualHosts."nullroot.${domain}" = { - onlySSL = true; + addSSL = true; enableACME = true; acmeRoot = null; }; }; + + # Test that a adding a second virtual host will not trigger + # other units (account and renewal service for first) + zeroconf3.configuration = { + services.${serverName}.virtualHosts = { + "zeroconf.${domain}" = { + addSSL = true; + enableACME = true; + serverAliases = [ "zeroconf2.${domain}" ]; + }; + "zeroconf3.${domain}" = { + addSSL = true; + enableACME = true; + }; + }; + # We're doing something risky with the combination of the service unit being persistent + # that could end up that the timers do not trigger properly. Show that timers have the + # desired effect. + systemd.timers."acme-renew-zeroconf3.${domain}".timerConfig = { + OnCalendar = lib.mkForce "*-*-* *:*:0/5"; + AccuracySec = lib.mkForce 0; + # Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/. + RandomizedDelaySec = lib.mkForce 0; + FixedRandomDelay = lib.mkForce 0; + }; + }; }; }; }; @@ -121,30 +154,24 @@ ca_domain = "${nodes.acme.test-support.acme.caDomain}" fqdn = f"proxied.{domain}" + webserver.start() + webserver.wait_for_unit("${serverName}.service") + + with subtest("Can run on self-signed certificates"): + check_issuer(webserver, fqdn, "minica") + # Check that the web server has picked up the selfsigned cert + check_connection(webserver, fqdn, minica=True) + acme.start() wait_for_running(acme) acme.wait_for_open_port(443) with subtest("Acquire a cert through a proxied lego"): - webserver.start() - webserver.succeed("systemctl is-system-running --wait") - wait_for_running(webserver) - download_ca_certs(webserver, ca_domain) - check_connection(webserver, fqdn) - - with subtest("Can run on selfsigned certificates"): - # Switch to selfsigned first - webserver.succeed(f"systemctl clean acme-{fqdn}.service --what=state") - webserver.succeed(f"systemctl start acme-selfsigned-{fqdn}.service") - check_issuer(webserver, fqdn, "minica") - webserver.succeed("systemctl restart ${serverName}-config-reload.service") - # Check that the web server has picked up the selfsigned cert - check_connection(webserver, fqdn, minica=True) - webserver.succeed("systemctl stop renew-triggered.target") - webserver.succeed(f"systemctl start acme-{fqdn}.service") - webserver.wait_for_unit("renew-triggered.target") - check_issuer(webserver, fqdn, "pebble") - check_connection(webserver, fqdn) + webserver.succeed(f"systemctl start acme-order-renew-{fqdn}.service") + webserver.wait_for_unit("renew-triggered.target") + download_ca_certs(webserver, ca_domain) + check_issuer(webserver, fqdn, "pebble") + check_connection(webserver, fqdn) with subtest("security.acme changes reflect on web server part 1"): check_connection(webserver, f"certchange.{domain}", fail=True) @@ -181,5 +208,23 @@ switch_to(webserver, "nullroot") webserver.wait_for_unit("renew-triggered.target") check_connection(webserver, f"nullroot.{domain}") + + with subtest("Ensure that adding a second vhost does not trigger first vhost acme units"): + switch_to(webserver, "zeroconf") + webserver.wait_for_unit("renew-triggered.target") + webserver.succeed("journalctl --cursor-file=/tmp/cursor | grep acme") + switch_to(webserver, "zeroconf3") + webserver.wait_for_unit("renew-triggered.target") + output = webserver.succeed("journalctl --cursor-file=/tmp/cursor | grep acme") + # The new certificate unit gets triggered: + t.assertIn(f"acme-zeroconf3.{domain}-start", output) + # The account generation should not be triggered again: + t.assertNotIn("acme-account-d590213ed52603e9128d.target", output) + # The other certificates should also not be triggered: + t.assertNotIn(f"acme-zeroconf.{domain}-start", output) + t.assertNotIn(f"acme-proxied.{domain}-start", output) + # Ensure the timer works, due to our shenanigans with + # RemainAfterExit=true + webserver.wait_until_succeeds(f"journalctl --cursor-file=/tmp/cursor | grep 'Starting Order (and renew) ACME certificate for zeroconf3.{domain}...'") ''; } diff --git a/nixos/tests/step-ca.nix b/nixos/tests/step-ca.nix index 69b22bfd424a..ebd64dbda297 100644 --- a/nixos/tests/step-ca.nix +++ b/nixos/tests/step-ca.nix @@ -137,17 +137,18 @@ import ./make-test-python.nix ( caserver.wait_for_unit("step-ca.service") caserver.wait_until_succeeds("journalctl -o cat -u step-ca.service | grep '${pkgs.step-ca.version}'") - caclient.wait_for_unit("acme-finished-caclient.target") - catester.succeed("curl https://caclient/ | grep \"Welcome to nginx!\"") + caclient.wait_for_unit("acme-caclient.service") + # The order is run asynchonously, keep trying. + catester.wait_until_succeeds("curl https://caclient/ | grep \"Welcome to nginx!\"") caclientcaddy.wait_for_unit("caddy.service") # It’s hard to know when Caddy has finished the ACME dance with # step-ca, so we keep trying cURL until success. catester.wait_until_succeeds("curl https://caclientcaddy/ | grep \"Welcome to Caddy!\"") - caclienth2o.wait_for_unit("acme-finished-caclienth2o.target") + caclienth2o.wait_for_unit("acme-caclienth2o.service") caclienth2o.wait_for_unit("h2o.service") - catester.succeed("curl https://caclienth2o/ | grep \"Welcome to H2O!\"") + catester.wait_until_succeeds("curl https://caclienth2o/ | grep \"Welcome to H2O!\"") ''; } )