From 229640ed3a1d0a1c637a729f0db1f482c1ee063b Mon Sep 17 00:00:00 2001 From: Lucas Savva Date: Mon, 20 Jan 2025 16:38:12 +0000 Subject: [PATCH] nixos/tests/acme: Refactor test suite Split tests up based on certain use cases: - http01-builtin: Tests most functionality of the core module, such as the systemd and hashing components, whilst utilising lego's built in http01 resolution mechanis. - dns01: Tests only that DNS01 renewal works as expected. - nginx: Tests nginx compatability - httpd: Tests httpd compatability - caddy: Tests caddy compatability --- nixos/modules/security/acme/default.nix | 32 +- nixos/release-combined.nix | 3 +- nixos/release-small.nix | 8 +- nixos/tests/acme.nix | 788 --------------------- nixos/tests/acme/caddy.nix | 120 ++++ nixos/tests/acme/default.nix | 56 ++ nixos/tests/acme/dns01.nix | 118 +++ nixos/tests/acme/http01-builtin.nix | 215 ++++++ nixos/tests/acme/python-utils.py | 166 +++++ nixos/tests/acme/utils.nix | 4 + nixos/tests/acme/webserver.nix | 185 +++++ nixos/tests/all-tests.nix | 2 +- nixos/tests/common/acme/server/default.nix | 111 ++- nixos/tests/common/resolver.nix | 184 ----- pkgs/by-name/ca/caddy/package.nix | 1 + pkgs/by-name/le/lego/package.nix | 5 +- pkgs/by-name/pe/pebble/package.nix | 3 +- pkgs/servers/http/apache-httpd/2.4.nix | 2 +- pkgs/servers/http/nginx/generic.nix | 2 +- 19 files changed, 944 insertions(+), 1061 deletions(-) delete mode 100644 nixos/tests/acme.nix create mode 100644 nixos/tests/acme/caddy.nix create mode 100644 nixos/tests/acme/default.nix create mode 100644 nixos/tests/acme/dns01.nix create mode 100644 nixos/tests/acme/http01-builtin.nix create mode 100644 nixos/tests/acme/python-utils.py create mode 100644 nixos/tests/acme/utils.nix create mode 100644 nixos/tests/acme/webserver.nix delete mode 100644 nixos/tests/common/resolver.nix diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix index bd3cbf279ea3..d829f5717622 100644 --- a/nixos/modules/security/acme/default.nix +++ b/nixos/modules/security/acme/default.nix @@ -117,6 +117,7 @@ let ${lib.optionalString cfg.defaults.enableDebugLogs "set -x"} set -euo pipefail cd /var/lib/acme + chmod -R u=rwX,g=,o= .lego/accounts chown -R ${user} .lego/accounts '' + (lib.concatStringsSep "\n" (lib.mapAttrsToList (cert: data: '' for fixpath in ${lib.escapeShellArg cert} .lego/${lib.escapeShellArg cert}; do @@ -161,8 +162,7 @@ let }; } - # In order to avoid race conditions creating the CA for selfsigned certs, - # we have a separate service which will create the necessary files. + # Avoid race conditions creating the CA for selfsigned certs (lib.mkIf cfg.preliminarySelfsigned { path = [ pkgs.minica ]; # Working directory will be /tmp @@ -482,6 +482,9 @@ let # By default group will have no access to the cert files. # This chmod will fix that. chmod 640 out/* + + # Also ensure safer permissions on the account directory. + chmod -R u=rwX,g=,o= accounts/. ''; }; }; @@ -599,6 +602,17 @@ let ''; }; + listenHTTP = lib.mkOption { + type = lib.types.nullOr lib.types.str; + inherit (defaultAndText "listenHTTP" null) default defaultText; + example = ":1360"; + description = '' + Interface and port to listen on to solve HTTP challenges + in the form `[INTERFACE]:PORT`. + If you use a port other than 80, you must proxy port 80 to this port. + ''; + }; + dnsProvider = lib.mkOption { type = lib.types.nullOr lib.types.str; inherit (defaultAndText "dnsProvider" null) default defaultText; @@ -744,20 +758,6 @@ let ''; }; - # This setting must be different for each configured certificate, otherwise - # two or more renewals may fail to bind to the address. Hence, it is not in - # the inheritableOpts. - listenHTTP = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = ":1360"; - description = '' - Interface and port to listen on to solve HTTP challenges - in the form [INTERFACE]:PORT. - If you use a port other than 80, you must proxy port 80 to this port. - ''; - }; - s3Bucket = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix index 10811f63e5eb..4167241b9e76 100644 --- a/nixos/release-combined.nix +++ b/nixos/release-combined.nix @@ -75,7 +75,8 @@ rec { (onFullSupported "nixos.iso_gnome") (onFullSupported "nixos.manual") (onSystems [ "aarch64-linux" ] "nixos.sd_image") - (onFullSupported "nixos.tests.acme") + (onFullSupported "nixos.tests.acme.http01-builtin") + (onFullSupported "nixos.tests.acme.dns01") (onSystems [ "x86_64-linux" ] "nixos.tests.boot.biosCdrom") (onSystems [ "x86_64-linux" ] "nixos.tests.boot.biosUsb") (onFullSupported "nixos.tests.boot-stage1") diff --git a/nixos/release-small.nix b/nixos/release-small.nix index 27857b0cdfd2..e29070219ef0 100644 --- a/nixos/release-small.nix +++ b/nixos/release-small.nix @@ -48,8 +48,11 @@ rec { dummy ; tests = { + inherit (nixos'.tests.acme) + http01-builtin + dns01 + ; inherit (nixos'.tests) - acme containers-imperative containers-ip firewall @@ -135,7 +138,8 @@ rec { (map onSupported [ "nixos.dummy" "nixos.manual" - "nixos.tests.acme" + "nixos.tests.acme.http01-builtin" + "nixos.tests.acme.dns01" "nixos.tests.containers-imperative" "nixos.tests.containers-ip" "nixos.tests.firewall" diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix deleted file mode 100644 index 308032cccc45..000000000000 --- a/nixos/tests/acme.nix +++ /dev/null @@ -1,788 +0,0 @@ -{ config, lib, ... }: let - - pkgs = config.node.pkgs; - - commonConfig = ./common/acme/client; - - dnsServerIP = nodes: nodes.dnsserver.networking.primaryIPAddress; - - dnsScript = nodes: let - dnsAddress = dnsServerIP nodes; - in pkgs.writeShellScript "dns-hook.sh" '' - set -euo pipefail - echo '[INFO]' "[$2]" 'dns-hook.sh' $* - if [ "$1" = "present" ]; then - ${pkgs.curl}/bin/curl --data '{"host": "'"$2"'", "value": "'"$3"'"}' http://${dnsAddress}:8055/set-txt - else - ${pkgs.curl}/bin/curl --data '{"host": "'"$2"'"}' http://${dnsAddress}:8055/clear-txt - fi - ''; - - dnsConfig = nodes: { - dnsProvider = "exec"; - dnsPropagationCheck = false; - environmentFile = pkgs.writeText "wildcard.env" '' - EXEC_PATH=${dnsScript nodes} - EXEC_POLLING_INTERVAL=1 - EXEC_PROPAGATION_TIMEOUT=1 - EXEC_SEQUENCE_INTERVAL=1 - ''; - }; - - documentRoot = pkgs.runCommand "docroot" {} '' - mkdir -p "$out" - echo hello world > "$out/index.html" - ''; - - vhostBase = { - forceSSL = true; - locations."/".root = documentRoot; - }; - - vhostBaseHttpd = { - forceSSL = true; - inherit documentRoot; - }; - - simpleConfig = { - security.acme = { - certs."http.example.test" = { - listenHTTP = ":80"; - }; - }; - - networking.firewall.allowedTCPPorts = [ 80 ]; - }; - - # Base specialisation config for testing general ACME features - webserverBasicConfig = { - services.nginx.enable = true; - services.nginx.virtualHosts."a.example.test" = vhostBase // { - enableACME = true; - }; - }; - - # Generate specialisations for testing a web server - mkServerConfigs = { server, group, vhostBaseData, extraConfig ? {} }: let - baseConfig = { nodes, config, specialConfig ? {} }: lib.mkMerge [ - { - security.acme = { - defaults = (dnsConfig nodes); - # One manual wildcard cert - certs."example.test" = { - domain = "*.example.test"; - }; - }; - - users.users."${config.services."${server}".user}".extraGroups = ["acme"]; - - services."${server}" = { - enable = true; - virtualHosts = { - # Run-of-the-mill vhost using HTTP-01 validation - "${server}-http.example.test" = vhostBaseData // { - serverAliases = [ "${server}-http-alias.example.test" ]; - enableACME = true; - }; - - # Another which inherits the DNS-01 config - "${server}-dns.example.test" = vhostBaseData // { - serverAliases = [ "${server}-dns-alias.example.test" ]; - enableACME = true; - # Set acmeRoot to null instead of using the default of "/var/lib/acme/acme-challenge" - # webroot + dnsProvider are mutually exclusive. - acmeRoot = null; - }; - - # One using the wildcard certificate - "${server}-wildcard.example.test" = vhostBaseData // { - serverAliases = [ "${server}-wildcard-alias.example.test" ]; - useACMEHost = "example.test"; - }; - } // (lib.optionalAttrs (server == "nginx") { - # The nginx module supports using a different key than the hostname - different-key = vhostBaseData // { - serverName = "${server}-different-key.example.test"; - serverAliases = [ "${server}-different-key-alias.example.test" ]; - enableACME = true; - }; - }); - }; - - # Used to determine if service reload was triggered - systemd.targets."test-renew-${server}" = { - wants = [ "acme-${server}-http.example.test.service" ]; - after = [ "acme-${server}-http.example.test.service" "${server}-config-reload.service" ]; - }; - } - specialConfig - extraConfig - ]; - in { - "${server}".configuration = { nodes, config, ... }: baseConfig { - inherit nodes config; - }; - - # Test that server reloads when an alias is removed (and subsequently test removal works in acme) - "${server}_remove_alias".configuration = { nodes, config, ... }: baseConfig { - inherit nodes config; - specialConfig = { - # Remove an alias, but create a standalone vhost in its place for testing. - # This configuration results in certificate errors as useACMEHost does not imply - # append extraDomains, and thus we can validate the SAN is removed. - services."${server}" = { - virtualHosts."${server}-http.example.test".serverAliases = lib.mkForce []; - virtualHosts."${server}-http-alias.example.test" = vhostBaseData // { - useACMEHost = "${server}-http.example.test"; - }; - }; - }; - }; - - # Test that the server reloads when only the acme configuration is changed. - "${server}_change_acme_conf".configuration = { nodes, config, ... }: baseConfig { - inherit nodes config; - specialConfig = { - security.acme.certs."${server}-http.example.test" = { - keyType = "ec384"; - # Also test that postRun is exec'd as root - postRun = "id | grep root"; - }; - }; - }; - }; - -in { - name = "acme"; - meta = { - maintainers = lib.teams.acme.members; - # Hard timeout in seconds. Average run time is about 7 minutes. - timeout = 1800; - }; - - nodes = { - # The fake ACME server which will respond to client requests - acme = { nodes, ... }: { - imports = [ ./common/acme/server ]; - networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ]; - }; - - # A fake DNS server which can be configured with records as desired - # Used to test DNS-01 challenge - dnsserver = { nodes, ... }: { - networking.firewall.allowedTCPPorts = [ 8055 53 ]; - networking.firewall.allowedUDPPorts = [ 53 ]; - systemd.services.pebble-challtestsrv = { - enable = true; - description = "Pebble ACME challenge test server"; - wantedBy = [ "network.target" ]; - serviceConfig = { - ExecStart = "${pkgs.pebble}/bin/pebble-challtestsrv -dns01 ':53' -defaultIPv6 '' -defaultIPv4 '${nodes.webserver.networking.primaryIPAddress}'"; - # Required to bind on privileged ports. - AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; - }; - }; - }; - - # A web server which will be the node requesting certs - webserver = { nodes, config, ... }: { - imports = [ commonConfig ]; - networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ]; - networking.firewall.allowedTCPPorts = [ 80 443 ]; - - # OpenSSL will be used for more thorough certificate validation - environment.systemPackages = [ pkgs.openssl ]; - - # Set log level to info so that we can see when the service is reloaded - services.nginx.logError = "stderr info"; - - specialisation = { - # Tests HTTP-01 verification using Lego's built-in web server - http01lego.configuration = simpleConfig; - - # account hash generation with default server from <= 23.11 - http01lego_legacyAccountHash.configuration = lib.mkMerge [ - simpleConfig - { - security.acme.defaults.server = lib.mkForce null; - } - ]; - - renew.configuration = lib.mkMerge [ - simpleConfig - { - # Pebble provides 5 year long certs, - # needs to be higher than that to test renewal - security.acme.certs."http.example.test".validMinDays = 9999; - } - ]; - - # Tests that account creds can be safely changed. - accountchange.configuration = lib.mkMerge [ - simpleConfig - { - security.acme.certs."http.example.test".email = "admin@example.test"; - } - ]; - - # First derivation used to test general ACME features - general.configuration = { ... }: let - caDomain = nodes.acme.test-support.acme.caDomain; - email = config.security.acme.defaults.email; - # Exit 99 to make it easier to track if this is the reason a renew failed - accountCreateTester = '' - test -e accounts/${caDomain}/${email}/account.json || exit 99 - ''; - in lib.mkMerge [ - webserverBasicConfig - { - # Used to test that account creation is collated into one service. - # These should not run until after acme-finished-a.example.test.target - systemd.services."b.example.test".preStart = accountCreateTester; - systemd.services."c.example.test".preStart = accountCreateTester; - - services.nginx.virtualHosts."b.example.test" = vhostBase // { - enableACME = true; - }; - services.nginx.virtualHosts."c.example.test" = vhostBase // { - enableACME = true; - }; - } - ]; - - # Test OCSP Stapling - ocsp_stapling.configuration = { ... }: lib.mkMerge [ - webserverBasicConfig - { - security.acme.certs."a.example.test".ocspMustStaple = true; - services.nginx.virtualHosts."a.example.test" = { - extraConfig = '' - ssl_stapling on; - ssl_stapling_verify on; - ''; - }; - } - ]; - - # Validate service relationships by adding a slow start service to nginx' wants. - # Reproducer for https://github.com/NixOS/nixpkgs/issues/81842 - slow_startup.configuration = { ... }: lib.mkMerge [ - webserverBasicConfig - { - systemd.services.my-slow-service = { - wantedBy = [ "multi-user.target" "nginx.service" ]; - before = [ "nginx.service" ]; - preStart = "sleep 5"; - script = "${pkgs.python3}/bin/python -m http.server"; - }; - - services.nginx.virtualHosts."slow.example.test" = { - forceSSL = true; - enableACME = true; - locations."/".proxyPass = "http://localhost:8000"; - }; - } - ]; - - concurrency_limit.configuration = {pkgs, ...}: lib.mkMerge [ - webserverBasicConfig { - security.acme.maxConcurrentRenewals = 1; - - services.nginx.virtualHosts = { - "f.example.test" = vhostBase // { - enableACME = true; - }; - "g.example.test" = vhostBase // { - enableACME = true; - }; - "h.example.test" = vhostBase // { - enableACME = true; - }; - }; - - systemd.services = { - # check for mutual exclusion of starting renew services - "acme-f.example.test".serviceConfig.ExecPreStart = "+" + (pkgs.writeShellScript "test-f" '' - test "$(systemctl is-active acme-{g,h}.example.test.service | grep activating | wc -l)" -le 0 - ''); - "acme-g.example.test".serviceConfig.ExecPreStart = "+" + (pkgs.writeShellScript "test-g" '' - test "$(systemctl is-active acme-{f,h}.example.test.service | grep activating | wc -l)" -le 0 - ''); - "acme-h.example.test".serviceConfig.ExecPreStart = "+" + (pkgs.writeShellScript "test-h" '' - test "$(systemctl is-active acme-{g,f}.example.test.service | grep activating | wc -l)" -le 0 - ''); - }; - } - ]; - - # Test lego internal server (listenHTTP option) - # Also tests useRoot option - lego_server.configuration = { ... }: { - security.acme.useRoot = true; - security.acme.certs."lego.example.test" = { - listenHTTP = ":80"; - group = "nginx"; - }; - services.nginx.enable = true; - services.nginx.virtualHosts."lego.example.test" = { - useACMEHost = "lego.example.test"; - onlySSL = true; - }; - }; - - # Test compatibility with Caddy - # It only supports useACMEHost, hence not using mkServerConfigs - } // (let - baseCaddyConfig = { nodes, config, ... }: { - security.acme = { - defaults = (dnsConfig nodes); - # One manual wildcard cert - certs."example.test" = { - domain = "*.example.test"; - }; - }; - - users.users."${config.services.caddy.user}".extraGroups = ["acme"]; - - services.caddy = { - enable = true; - virtualHosts."a.example.test" = { - useACMEHost = "example.test"; - extraConfig = '' - root * ${documentRoot} - ''; - }; - }; - }; - in { - caddy.configuration = baseCaddyConfig; - - # Test that the server reloads when only the acme configuration is changed. - "caddy_change_acme_conf".configuration = { nodes, config, ... }: lib.mkMerge [ - (baseCaddyConfig { - inherit nodes config; - }) - { - security.acme.certs."example.test" = { - keyType = "ec384"; - }; - } - ]; - - # Test compatibility with Nginx - }) // (mkServerConfigs { - server = "nginx"; - group = "nginx"; - vhostBaseData = vhostBase; - }) - - # Test compatibility with Apache HTTPD - // (mkServerConfigs { - server = "httpd"; - group = "wwwrun"; - vhostBaseData = vhostBaseHttpd; - extraConfig = { - services.httpd.adminAddr = config.security.acme.defaults.email; - }; - }); - }; - - # The client will be used to curl the webserver to validate configuration - client = { nodes, ... }: { - imports = [ commonConfig ]; - networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ]; - - # OpenSSL will be used for more thorough certificate validation - environment.systemPackages = [ pkgs.openssl ]; - }; - }; - - testScript = { nodes, ... }: - let - caDomain = nodes.acme.test-support.acme.caDomain; - in - # Note, wait_for_unit does not work for oneshot services that do not have RemainAfterExit=true, - # this is because a oneshot goes from inactive => activating => inactive, and never - # reaches the active state. Targets do not have this issue. - '' - import time - - TOTAL_RETRIES = 20 - - - class BackoffTracker(object): - 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 switch_to(node, name, allow_fail=False): - # On first switch, this will create a symlink to the current system so that we can - # quickly switch between derivations - root_specs = "/tmp/specialisation" - node.execute( - f"test -e {root_specs}" - f" || ln -s $(readlink /run/current-system)/specialisation {root_specs}" - ) - - switcher_path = ( - f"/run/current-system/specialisation/{name}/bin/switch-to-configuration" - ) - rc, _ = node.execute(f"test -e '{switcher_path}'") - if rc > 0: - switcher_path = f"/tmp/specialisation/{name}/bin/switch-to-configuration" - - if not allow_fail: - node.succeed( - f"{switcher_path} test" - ) - else: - node.execute( - f"{switcher_path} test" - ) - - # Start a unit explicitly, then wait for it to activate. - # This is used for the acme-finished-* targets, as those - # aren't started by switch-to-configuration, meaning - # wait_for_unit(target) will fail with "no pending jobs" - # if it wins the race and checks the target state before - # the actual unit is started. - def start_and_wait(node, unit): - node.start_job(unit) - node.wait_for_unit(unit) - - # Ensures the issuer of our cert matches the chain - # 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 - def check_issuer(node, cert_name, issuer): - for fname in ("cert.pem", "fullchain.pem"): - actual_issuer = node.succeed( - f"openssl x509 -noout -issuer -in /var/lib/acme/{cert_name}/{fname}" - ).partition("=")[2] - assert ( - issuer.lower() in actual_issuer.lower() - ), f"{fname} issuer mismatch. Expected {issuer} got {actual_issuer}" - - - # Ensure cert comes before chain in fullchain.pem - def check_fullchain(node, cert_name): - cert_file = f"/var/lib/acme/{cert_name}/fullchain.pem" - num_certs = node.succeed(f"grep -o 'END CERTIFICATE' {cert_file}") - assert len(num_certs.strip().split("\n")) > 1, "Insufficient certs in fullchain.pem" - - first_cert_data = node.succeed( - f"grep -m1 -B50 'END CERTIFICATE' {cert_file}" - " | openssl x509 -noout -text" - ) - for line in first_cert_data.lower().split("\n"): - if "dns:" in line: - print(f"First DNSName in fullchain.pem: {line}") - assert cert_name.lower() in line, f"{cert_name} not found in {line}" - return - - assert False - - - @backoff.protect - def check_connection(node, domain): - result = node.succeed( - "openssl s_client -brief -verify 2 -CAfile /tmp/ca.crt" - f" -servername {domain} -connect {domain}:443 < /dev/null 2>&1" - ) - - for line in result.lower().split("\n"): - assert not ( - "verification" in line and "error" in line - ), f"Failed to connect to https://{domain}" - - - @backoff.protect - def check_connection_key_bits(node, domain, bits): - result = node.succeed( - "openssl s_client -CAfile /tmp/ca.crt" - f" -servername {domain} -connect {domain}:443 < /dev/null" - " | openssl x509 -noout -text | grep -i Public-Key" - ) - print("Key type:", result) - - assert bits in result, f"Did not find expected number of bits ({bits}) in key" - - - @backoff.protect - def check_stapling(node, domain): - # Pebble doesn't provide a full OCSP responder, so just check the URL - result = node.succeed( - "openssl s_client -CAfile /tmp/ca.crt" - f" -servername {domain} -connect {domain}:443 < /dev/null" - " | openssl x509 -noout -ocsp_uri" - ) - print("OCSP Responder URL:", result) - - assert "${caDomain}:4002" in result.lower(), "OCSP Stapling check failed" - - - @backoff.protect - def download_ca_certs(node): - node.succeed("curl https://${caDomain}:15000/roots/0 > /tmp/ca.crt") - node.succeed("curl https://${caDomain}:15000/intermediate-keys/0 >> /tmp/ca.crt") - - - @backoff.protect - def set_a_record(node): - node.succeed( - 'curl --data \'{"host": "${caDomain}", "addresses": ["${nodes.acme.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a' - ) - - - start_all() - - dnsserver.wait_for_unit("pebble-challtestsrv.service") - client.wait_for_unit("default.target") - - set_a_record(client) - - acme.systemctl("start network-online.target") - acme.wait_for_unit("network-online.target") - acme.wait_for_unit("pebble.service") - - download_ca_certs(client) - - # Perform http-01 w/ lego test first - with subtest("Can request certificate with Lego's built in web server"): - switch_to(webserver, "http01lego") - start_and_wait(webserver, "acme-finished-http.example.test.target") - check_fullchain(webserver, "http.example.test") - check_issuer(webserver, "http.example.test", "pebble") - - # Perform account hash test - with subtest("Assert that account hash didn't unexpectedly change"): - hash = webserver.succeed("ls /var/lib/acme/.lego/accounts/") - print("Account hash: " + hash) - assert hash.strip() == "d590213ed52603e9128d" - - # Perform renewal test - with subtest("Can renew certificates when they expire"): - hash = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem") - switch_to(webserver, "renew") - start_and_wait(webserver, "acme-finished-http.example.test.target") - check_fullchain(webserver, "http.example.test") - check_issuer(webserver, "http.example.test", "pebble") - hash_after = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem") - assert hash != hash_after - - # Perform account change test - with subtest("Handles email change correctly"): - hash = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem") - switch_to(webserver, "accountchange") - start_and_wait(webserver, "acme-finished-http.example.test.target") - check_fullchain(webserver, "http.example.test") - check_issuer(webserver, "http.example.test", "pebble") - hash_after = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem") - # Has to do a full run to register account, which creates new certs. - assert hash != hash_after - - # Perform general tests - switch_to(webserver, "general") - - with subtest("Can request certificate with HTTP-01 challenge"): - start_and_wait(webserver, "acme-finished-a.example.test.target") - check_fullchain(webserver, "a.example.test") - check_issuer(webserver, "a.example.test", "pebble") - webserver.wait_for_unit("nginx.service") - check_connection(client, "a.example.test") - - with subtest("Runs 1 cert for account creation before others"): - start_and_wait(webserver, "acme-finished-b.example.test.target") - start_and_wait(webserver, "acme-finished-c.example.test.target") - check_connection(client, "b.example.test") - check_connection(client, "c.example.test") - - with subtest("Certificates and accounts have safe + valid permissions"): - # Nginx will set the group appropriately when enableACME is used - group = "nginx" - webserver.succeed( - f"test $(stat -L -c '%a %U %G' /var/lib/acme/a.example.test/*.pem | tee /dev/stderr | grep '640 acme {group}' | wc -l) -eq 5" - ) - webserver.succeed( - f"test $(stat -L -c '%a %U %G' /var/lib/acme/.lego/a.example.test/**/a.example.test* | tee /dev/stderr | grep '600 acme {group}' | wc -l) -eq 4" - ) - webserver.succeed( - f"test $(stat -L -c '%a %U %G' /var/lib/acme/a.example.test | tee /dev/stderr | grep '750 acme {group}' | wc -l) -eq 1" - ) - webserver.succeed( - f"test $(find /var/lib/acme/accounts -type f -exec stat -L -c '%a %U %G' {{}} \\; | tee /dev/stderr | grep -v '600 acme {group}' | wc -l) -eq 0" - ) - - # Selfsigned certs tests happen late so we aren't fighting the system init triggering cert renewal - with subtest("Can generate valid selfsigned certs"): - webserver.succeed("systemctl clean acme-a.example.test.service --what=state") - webserver.succeed("systemctl start acme-selfsigned-a.example.test.service") - check_fullchain(webserver, "a.example.test") - check_issuer(webserver, "a.example.test", "minica") - # Check selfsigned permissions - webserver.succeed( - f"test $(stat -L -c '%a %U %G' /var/lib/acme/a.example.test/*.pem | tee /dev/stderr | grep '640 acme {group}' | wc -l) -eq 5" - ) - # Will succeed if nginx can load the certs - webserver.succeed("systemctl start nginx-config-reload.service") - - with subtest("Correctly implements OCSP stapling"): - switch_to(webserver, "ocsp_stapling") - start_and_wait(webserver, "acme-finished-a.example.test.target") - check_stapling(client, "a.example.test") - - with subtest("Can request certificate with HTTP-01 using lego's internal web server"): - switch_to(webserver, "lego_server") - start_and_wait(webserver, "acme-finished-lego.example.test.target") - webserver.wait_for_unit("nginx.service") - webserver.succeed("echo HENLO && systemctl cat nginx.service") - webserver.succeed('test "$(stat -c \'%U\' /var/lib/acme/* | uniq)" = "root"') - check_connection(client, "a.example.test") - check_connection(client, "lego.example.test") - - with subtest("Can request certificate with HTTP-01 when nginx startup is delayed"): - webserver.execute("systemctl stop nginx") - switch_to(webserver, "slow_startup") - start_and_wait(webserver, "acme-finished-slow.example.test.target") - check_issuer(webserver, "slow.example.test", "pebble") - webserver.wait_for_unit("nginx.service") - check_connection(client, "slow.example.test") - - with subtest("Can limit concurrency of running renewals"): - switch_to(webserver, "concurrency_limit") - start_and_wait(webserver, "acme-finished-f.example.test.target") - start_and_wait(webserver, "acme-finished-g.example.test.target") - start_and_wait(webserver, "acme-finished-h.example.test.target") - check_connection(client, "f.example.test") - check_connection(client, "g.example.test") - check_connection(client, "h.example.test") - - with subtest("Works with caddy"): - switch_to(webserver, "caddy") - start_and_wait(webserver, "acme-finished-example.test.target") - webserver.wait_for_unit("caddy.service") - # FIXME reloading caddy is not sufficient to load new certs. - # Restart it manually until this is fixed. - webserver.succeed("systemctl restart caddy.service") - check_connection(client, "a.example.test") - - with subtest("security.acme changes reflect on caddy"): - switch_to(webserver, "caddy_change_acme_conf") - start_and_wait(webserver, "acme-finished-example.test.target") - webserver.wait_for_unit("caddy.service") - # FIXME reloading caddy is not sufficient to load new certs. - # Restart it manually until this is fixed. - webserver.succeed("systemctl restart caddy.service") - check_connection_key_bits(client, "a.example.test", "384") - - common_domains = ["http", "dns", "wildcard"] - for server, logsrc, domains in [ - ("nginx", "journalctl -n 30 -u nginx.service", common_domains + ["different-key"]), - ("httpd", "tail -n 30 /var/log/httpd/*.log", common_domains), - ]: - wait_for_server = lambda: webserver.wait_for_unit(f"{server}.service") - with subtest(f"Works with {server}"): - try: - switch_to(webserver, server) - for domain in domains: - if domain != "wildcard": - start_and_wait( - webserver, - f"acme-finished-{server}-{domain}.example.test.target" - ) - except Exception as err: - _, output = webserver.execute( - f"{logsrc} && ls -al /var/lib/acme/acme-challenge" - ) - print(output) - raise err - - wait_for_server() - - for domain in domains: - if domain != "wildcard": - check_issuer(webserver, f"{server}-{domain}.example.test", "pebble") - for domain in domains: - check_connection(client, f"{server}-{domain}.example.test") - check_connection(client, f"{server}-{domain}-alias.example.test") - - test_domain = f"{server}-{domains[0]}.example.test" - - with subtest(f"Can reload {server} when timer triggers renewal"): - # Switch to selfsigned first - webserver.succeed(f"systemctl clean acme-{test_domain}.service --what=state") - webserver.succeed(f"systemctl start acme-selfsigned-{test_domain}.service") - check_issuer(webserver, test_domain, "minica") - webserver.succeed(f"systemctl start {server}-config-reload.service") - webserver.succeed(f"systemctl start test-renew-{server}.target") - check_issuer(webserver, test_domain, "pebble") - check_connection(client, test_domain) - - with subtest("Can remove an alias from a domain + cert is updated"): - test_alias = f"{server}-{domains[0]}-alias.example.test" - switch_to(webserver, f"{server}_remove_alias") - wait_for_server() - start_and_wait(webserver, f"acme-finished-{test_domain}.target") - wait_for_server() - check_connection(client, test_domain) - rc, _s = client.execute( - f"openssl s_client -CAfile /tmp/ca.crt -connect {test_alias}:443" - " /dev/null | openssl x509 -noout -text" - f" | grep DNS: | grep {test_alias}" - ) - assert rc > 0, "Removed extraDomainName was not removed from the cert" - - with subtest("security.acme changes reflect on web server"): - # Switch back to normal server config first, reset everything. - switch_to(webserver, server) - wait_for_server() - switch_to(webserver, f"{server}_change_acme_conf") - start_and_wait(webserver, f"acme-finished-{test_domain}.target") - wait_for_server() - check_connection_key_bits(client, test_domain, "384") - - # Perform http-01 w/ lego test again, but using the pre-24.05 account hashing - # (see https://github.com/NixOS/nixpkgs/pull/317257) - with subtest("Check account hashing compatibility with pre-24.05 settings"): - webserver.succeed("rm -rf /var/lib/acme/.lego/accounts/*") - switch_to(webserver, "http01lego_legacyAccountHash", allow_fail=True) - # unit is failed, but in a way that this throws no exception: - try: - start_and_wait(webserver, "acme-finished-http.example.test.target") - except Exception: - # The unit is allowed – or even expected – to fail due to not being able to - # reach the actual letsencrypt server. We only use it for serialising the - # test execution, such that the account check is done after the service run - # involving the account creation has been executed at least once. - pass - hash = webserver.succeed("ls /var/lib/acme/.lego/accounts/") - print("Account hash: " + hash) - assert hash.strip() == "1ccf607d9aa280e9af00" - ''; -} diff --git a/nixos/tests/acme/caddy.nix b/nixos/tests/acme/caddy.nix new file mode 100644 index 000000000000..96c86086d50a --- /dev/null +++ b/nixos/tests/acme/caddy.nix @@ -0,0 +1,120 @@ +{ + config, + lib, + pkgs, + ... +}: +let + domain = "example.test"; +in +{ + # Caddy only supports useACMEHost, hence we use a distinct test suite + name = "caddy"; + meta = { + maintainers = lib.teams.acme.members; + # Hard timeout in seconds. Average run time is about 60 seconds. + timeout = 180; + }; + + nodes = { + # The fake ACME server which will respond to client requests + acme = + { nodes, ... }: + { + imports = [ ../common/acme/server ]; + }; + + caddy = + { nodes, config, ... }: + let + fqdn = config.networking.fqdn; + in + { + imports = [ ../common/acme/client ]; + networking.domain = domain; + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + + # Resolve the vhosts the easy way + networking.hosts."127.0.0.1" = [ + "caddy-alt.${domain}" + ]; + + # OpenSSL will be used for more thorough certificate validation + environment.systemPackages = [ pkgs.openssl ]; + + security.acme.certs."${fqdn}" = { + listenHTTP = ":8080"; + reloadServices = [ "caddy.service" ]; + }; + + users.users."${config.services.caddy.user}".extraGroups = [ "acme" ]; + + services.caddy = { + enable = true; + # FIXME reloading caddy is not sufficient to load new certs. + # Restart it manually until this is fixed. + enableReload = false; + globalConfig = '' + auto_https off + ''; + virtualHosts."${fqdn}:443" = { + useACMEHost = fqdn; + }; + virtualHosts.":80".extraConfig = '' + reverse_proxy localhost:8080 + ''; + }; + + specialisation.add_domain.configuration = { + security.acme.certs.${fqdn}.extraDomainNames = [ + "caddy-alt.${domain}" + ]; + }; + }; + }; + + testScript = + { nodes, ... }: + '' + ${(import ./utils.nix).pythonUtils} + + domain = "${domain}" + ca_domain = "${nodes.acme.test-support.acme.caDomain}" + fqdn = "${nodes.caddy.networking.fqdn}" + + 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) + + 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") + check_connection(caddy, f"caddy-alt.{domain}") + ''; +} diff --git a/nixos/tests/acme/default.nix b/nixos/tests/acme/default.nix new file mode 100644 index 000000000000..7f861be2a027 --- /dev/null +++ b/nixos/tests/acme/default.nix @@ -0,0 +1,56 @@ +{ runTest }: +{ + http01-builtin = runTest ./http01-builtin.nix; + dns01 = runTest ./dns01.nix; + caddy = runTest ./caddy.nix; + nginx = runTest ( + import ./webserver.nix { + serverName = "nginx"; + group = "nginx"; + baseModule = { + services.nginx = { + enable = true; + enableReload = true; + logError = "stderr info"; + # This tests a number of things at once: + # - Self-signed certs are in place before the webserver startup + # - Nginx is started before acme renewal is attempted + # - useACMEHost behaves as expected + # - acmeFallbackHost behaves as expected + virtualHosts.default = { + default = true; + 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; + ''; + }; + }; + }; + } + ); + httpd = runTest ( + import ./webserver.nix { + serverName = "httpd"; + group = "wwwrun"; + baseModule = { + services.httpd = { + enable = true; + # This is the default by virtue of being the first defined vhost. + virtualHosts.default = { + addSSL = true; + useACMEHost = "proxied.example.test"; + locations."/.well-known/acme-challenge" = { + proxyPass = "http://localhost:8080/.well-known/acme-challenge"; + extraConfig = '' + ProxyPreserveHost On + ''; + }; + }; + }; + }; + } + ); +} diff --git a/nixos/tests/acme/dns01.nix b/nixos/tests/acme/dns01.nix new file mode 100644 index 000000000000..a6d4cedd0e57 --- /dev/null +++ b/nixos/tests/acme/dns01.nix @@ -0,0 +1,118 @@ +{ + config, + lib, + pkgs, + ... +}: +let + domain = "example.test"; + + dnsServerIP = nodes: nodes.dnsserver.networking.primaryIPAddress; + + dnsScript = pkgs.writeShellScript "dns-hook.sh" '' + set -euo pipefail + echo '[INFO]' "[$2]" 'dns-hook.sh' $* + if [ "$1" = "present" ]; then + ${pkgs.curl}/bin/curl --data @- http://dnsserver.test:8055/set-txt << EOF + {"host": "$2", "value": "$3"} + EOF + else + ${pkgs.curl}/bin/curl --data @- http://dnsserver.test:8055/clear-txt << EOF + {"host": "$2"} + EOF + fi + ''; +in +{ + name = "dns01"; + meta = { + maintainers = lib.teams.acme.members; + # Hard timeout in seconds. Average run time is about 60 seconds. + timeout = 180; + }; + + nodes = { + # The fake ACME server which will respond to client requests + acme = + { nodes, ... }: + { + imports = [ ../common/acme/server ]; + networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ]; + }; + + # A fake DNS server which can be configured with records as desired + # Used to test DNS-01 challenge + dnsserver = + { nodes, ... }: + { + networking = { + firewall.allowedTCPPorts = [ + 8055 + 53 + ]; + firewall.allowedUDPPorts = [ 53 ]; + + # nixos/lib/testing/network.nix will provide name resolution via /etc/hosts + # for all nodes based on their host names and domain + hostName = "dnsserver"; + domain = "test"; + }; + systemd.services.pebble-challtestsrv = { + enable = true; + description = "Pebble ACME challenge test server"; + wantedBy = [ "network.target" ]; + serviceConfig = { + ExecStart = "${pkgs.pebble}/bin/pebble-challtestsrv -dns01 ':53' -defaultIPv6 '' -defaultIPv4 '${nodes.client.networking.primaryIPAddress}'"; + # Required to bind on privileged ports. + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + }; + }; + }; + + client = + { nodes, ... }: + { + imports = [ ../common/acme/client ]; + networking.domain = domain; + networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ]; + + # OpenSSL will be used for more thorough certificate validation + environment.systemPackages = [ pkgs.openssl ]; + + security.acme.certs."${domain}" = { + domain = "*.${domain}"; + dnsProvider = "exec"; + dnsPropagationCheck = false; + environmentFile = pkgs.writeText "wildcard.env" '' + EXEC_PATH=${dnsScript} + EXEC_POLLING_INTERVAL=1 + EXEC_PROPAGATION_TIMEOUT=1 + EXEC_SEQUENCE_INTERVAL=1 + ''; + }; + }; + }; + + testScript = '' + ${(import ./utils.nix).pythonUtils} + + cert = "${domain}" + + dnsserver.start() + acme.start() + + wait_for_running(dnsserver) + dnsserver.wait_for_open_port(53) + wait_for_running(acme) + acme.wait_for_open_port(443) + + with subtest("Boot and acquire a new cert"): + client.start() + wait_for_running(client) + + check_issuer(client, cert, "pebble") + check_domain(client, cert, cert, fail=True) + check_domain(client, cert, f"toodeep.nesting.{cert}", fail=True) + check_domain(client, cert, f"whatever.{cert}") + ''; +} diff --git a/nixos/tests/acme/http01-builtin.nix b/nixos/tests/acme/http01-builtin.nix new file mode 100644 index 000000000000..ddc784128fbd --- /dev/null +++ b/nixos/tests/acme/http01-builtin.nix @@ -0,0 +1,215 @@ +{ + config, + lib, + pkgs, + ... +}: +let + domain = "example.test"; +in +{ + name = "http01-builtin"; + meta = { + maintainers = lib.teams.acme.members; + # Hard timeout in seconds. Average run time is about 90 seconds. + timeout = 300; + }; + + nodes = { + # The fake ACME server which will respond to client requests + acme = + { nodes, ... }: + { + imports = [ ../common/acme/server ]; + }; + + builtin = + { nodes, config, ... }: + { + imports = [ ../common/acme/client ]; + networking.domain = domain; + networking.firewall.allowedTCPPorts = [ 80 ]; + + # OpenSSL will be used for more thorough certificate validation + environment.systemPackages = [ pkgs.openssl ]; + + security.acme.certs."${config.networking.fqdn}" = { + listenHTTP = ":80"; + }; + + specialisation = { + renew.configuration = { + # Pebble provides 5 year long certs, + # needs to be higher than that to test renewal + security.acme.certs."${config.networking.fqdn}".validMinDays = 9999; + }; + + accountchange.configuration = { + security.acme.certs."${config.networking.fqdn}".email = "admin@example.test"; + }; + + keytype.configuration = { + security.acme.certs."${config.networking.fqdn}".keyType = "ec384"; + }; + + # Perform http-01 test again, but using the pre-24.05 account hashing + # (see https://github.com/NixOS/nixpkgs/pull/317257) + # The hash is deterministic in this case - only based on keyType and email. + # Note: This test is making the assumption that the acme module will create + # the account directory regardless of internet connectivity or server reachability. + legacy_account_hash.configuration = { + security.acme.defaults.server = lib.mkForce null; + }; + + ocsp_stapling.configuration = { + security.acme.certs."${config.networking.fqdn}".ocspMustStaple = true; + }; + + preservation.configuration = { }; + + add_cert_and_domain.configuration = { + security.acme.certs = { + "${config.networking.fqdn}" = { + extraDomainNames = [ + "builtin-alt.${domain}" + ]; + }; + # We can assume that if renewal succeeds then the account creation leader + # logic is working, since only one service could bind to port 80 at the same time. + "builtin-2.${domain}".listenHTTP = ":80"; + }; + # To make sure it's the account creation leader that is doing the work. + security.acme.maxConcurrentRenewals = 10; + }; + + concurrency.configuration = { + # As above, relying on port binding behaviour to assert that concurrency limit + # prevents > 1 service running at a time. + security.acme.maxConcurrentRenewals = 1; + security.acme.certs = { + "${config.networking.fqdn}" = { + extraDomainNames = [ + "builtin-alt.${domain}" + ]; + }; + "builtin-2.${domain}" = { + extraDomainNames = [ "builtin-2-alt.${domain}" ]; + listenHTTP = ":80"; + }; + "builtin-3.${domain}".listenHTTP = ":80"; + }; + }; + }; + }; + }; + + testScript = + { nodes, ... }: + let + certName = nodes.builtin.networking.fqdn; + caDomain = nodes.acme.test-support.acme.caDomain; + in + '' + ${(import ./utils.nix).pythonUtils} + + domain = "${domain}" + cert = "${certName}" + cert2 = "builtin-2." + domain + cert3 = "builtin-3." + domain + legacy_account_dir = "/var/lib/acme/.lego/accounts/1ccf607d9aa280e9af00" + + acme.start() + wait_for_running(acme) + acme.wait_for_open_port(443) + + with subtest("Boot and acquire a new cert"): + builtin.start() + wait_for_running(builtin) + + check_issuer(builtin, cert, "pebble") + check_domain(builtin, cert, cert) + + with subtest("Validate permissions"): + check_permissions(builtin, cert, "acme") + + with subtest("Check renewal behaviour"): + # First, test no-op behaviour + hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem") + # old_hash will be used in the preservation tests later + old_hash = hash + builtin.succeed(f"systemctl start acme-{cert}.service") + hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem") + assert hash == hash_after, "Certificate was unexpectedly changed" + + switch_to(builtin, "renew") + 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" + + with subtest("Handles email change correctly"): + hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem") + switch_to(builtin, "accountchange") + 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") + hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem") + # Has to do a full run to register account, which creates new certs. + assert hash != hash_after, "Certificate was not renewed" + # Remove the new account directory + builtin.succeed( + "cd /var/lib/acme/.lego/accounts" + " && ls -1 --sort=time | tee /dev/stderr | head -1 | xargs rm -rf" + ) + # old_hash will be used in the preservation tests later + old_hash = hash_after + + with subtest("Correctly implements OCSP stapling"): + check_stapling(builtin, cert, "${caDomain}", fail=True) + switch_to(builtin, "ocsp_stapling") + check_stapling(builtin, cert, "${caDomain}") + + with subtest("Handles keyType change correctly"): + check_key_bits(builtin, cert, 256) + switch_to(builtin, "keytype") + 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") + + 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" + switch_to(builtin, "preservation") + 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" + + with subtest("Add a new cert, extend existing cert domains"): + check_domain(builtin, cert, f"builtin-alt.{domain}", fail=True) + switch_to(builtin, "add_cert_and_domain") + 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") + + 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}") + + with subtest("Ensure Concurrency limits work"): + switch_to(builtin, "concurrency") + check_issuer(builtin, cert3, "pebble") + check_domain(builtin, cert3, cert3) + + with subtest("Generate self-signed certs"): + check_issuer(builtin, cert, "pebble") + builtin.succeed(f"systemctl clean acme-{cert}.service --what=state") + builtin.succeed(f"systemctl start acme-selfsigned-{cert}.service") + check_issuer(builtin, cert, "minica") + check_domain(builtin, cert, cert) + + with subtest("Validate permissions (self-signed)"): + check_permissions(builtin, cert, "acme") + ''; +} diff --git a/nixos/tests/acme/python-utils.py b/nixos/tests/acme/python-utils.py new file mode 100644 index 000000000000..77e7066e0349 --- /dev/null +++ b/nixos/tests/acme/python-utils.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +import time + +TOTAL_RETRIES = 20 + + +def run(node, cmd, fail=False): + if fail: + return node.fail(cmd) + else: + return node.succeed(cmd) + +# Waits for the system to finish booting or switching configuration +def wait_for_running(node): + node.succeed("systemctl is-system-running --wait") + +# On first switch, this will create a symlink to the current system so that we can +# quickly switch between derivations +def switch_to(node, name, fail=False) -> None: + root_specs = "/tmp/specialisation" + node.execute( + f"test -e {root_specs}" + f" || ln -s $(readlink /run/current-system)/specialisation {root_specs}" + ) + + switcher_path = ( + f"/run/current-system/specialisation/{name}/bin/switch-to-configuration" + ) + rc, _ = node.execute(f"test -e '{switcher_path}'") + if rc > 0: + switcher_path = f"/tmp/specialisation/{name}/bin/switch-to-configuration" + + cmd = f"{switcher_path} test" + run(node, cmd, fail=fail) + if not fail: + wait_for_running(node) + +# Ensures the issuer of our cert matches the chain +# 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 +def check_issuer(node, cert_name, issuer) -> None: + for fname in ("cert.pem", "fullchain.pem"): + actual_issuer = node.succeed( + f"openssl x509 -noout -issuer -in /var/lib/acme/{cert_name}/{fname}" + ).partition("=")[2] + assert ( + issuer.lower() in actual_issuer.lower() + ), f"{fname} issuer mismatch. Expected {issuer} got {actual_issuer}" + +# Ensures the provided domain matches with the given cert +def check_domain(node, cert_name, domain, fail=False) -> None: + cmd = f"openssl x509 -noout -checkhost '{domain}' -in /var/lib/acme/{cert_name}/cert.pem" + run(node, cmd, fail=fail) + +# Ensures the required values for OCSP stapling are present +# Pebble doesn't provide a full OCSP responder, so just checks the URL +def check_stapling(node, cert_name, ca_domain, fail=False): + rc, _ = node.execute( + f"openssl x509 -noout -ocsp_uri -in /var/lib/acme/{cert_name}/cert.pem" + f" | grep -i 'http://{ca_domain}:4002' 2>&1", + ) + assert rc == 0 or fail, "Failed to find OCSP URI in issued certificate" + run( + node, + f"openssl x509 -noout -ext tlsfeature -in /var/lib/acme/{cert_name}/cert.pem" + f" | grep -iv 'no extensions' 2>&1", + fail=fail, + ) + +# Checks the keyType by validating the number of bits +def check_key_bits(node, cert_name, bits, fail=False): + run( + node, + f"openssl x509 -noout -text -in /var/lib/acme/{cert_name}/cert.pem" + f" | grep -i Public-Key | grep {bits} | tee /dev/stderr", + fail=fail, + ) + +# Ensure cert comes before chain in fullchain.pem +def check_fullchain(node, cert_name): + cert_file = f"/var/lib/acme/{cert_name}/fullchain.pem" + num_certs = node.succeed(f"grep -o 'END CERTIFICATE' {cert_file}") + assert len(num_certs.strip().split("\n")) > 1, "Insufficient certs in fullchain.pem" + + first_cert_data = node.succeed( + f"grep -m1 -B50 'END CERTIFICATE' {cert_file}" + " | openssl x509 -noout -text" + ) + for line in first_cert_data.lower().split("\n"): + if "dns:" in line: + print(f"First DNSName in fullchain.pem: {line}") + assert cert_name.lower() in line, f"{cert_name} not found in {line}" + return + + assert False + +# Checks the permissions in the cert directories are as expected +def check_permissions(node, cert_name, group): + stat = "stat -L -c '%a %U %G' " + node.succeed( + f"test $({stat} /var/lib/acme/{cert_name}/*.pem" + f" | tee /dev/stderr | grep -v '640 acme {group}' | wc -l) -eq 0" + ) + 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" + ) + node.succeed( + f"test $({stat} /var/lib/acme/{cert_name}" + f" | tee /dev/stderr | grep -v '750 acme {group}' | wc -l) -eq 0" + ) + node.succeed( + f"test $(find /var/lib/acme/.lego/accounts -type f -exec {stat} {{}} \\;" + 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): + node.succeed(f"curl https://{ca_domain}:15000/roots/0 > /tmp/ca.crt") + node.succeed(f"curl https://{ca_domain}:15000/intermediate-keys/0 >> /tmp/ca.crt") + + +@backoff.protect +def check_connection(node, domain, fail=False, minica=False): + cafile = "/tmp/ca.crt" + if minica: + cafile = "/var/lib/acme/.minica/cert.pem" + run(node, + f"openssl s_client -brief -CAfile {cafile}" + f" -verify 2 -verify_return_error -verify_hostname {domain}" + f" -servername {domain} -connect {domain}:443 < /dev/null", + fail=fail, + ) diff --git a/nixos/tests/acme/utils.nix b/nixos/tests/acme/utils.nix new file mode 100644 index 000000000000..aae6af1ede93 --- /dev/null +++ b/nixos/tests/acme/utils.nix @@ -0,0 +1,4 @@ +{ + # Helper functions for python + pythonUtils = builtins.readFile ./python-utils.py; +} diff --git a/nixos/tests/acme/webserver.nix b/nixos/tests/acme/webserver.nix new file mode 100644 index 000000000000..a19f82edc76f --- /dev/null +++ b/nixos/tests/acme/webserver.nix @@ -0,0 +1,185 @@ +{ + serverName, + group, + baseModule, + domain ? "example.test", +}: +{ + config, + lib, + pkgs, + ... +}: +{ + name = serverName; + meta = { + maintainers = lib.teams.acme.members; + # Hard timeout in seconds. Average run time is about 100 seconds. + timeout = 300; + }; + + nodes = { + # The fake ACME server which will respond to client requests + acme = + { nodes, ... }: + { + imports = [ ../common/acme/server ]; + }; + + webserver = + { nodes, ... }: + { + imports = [ + ../common/acme/client + baseModule + ]; + networking.domain = domain; + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + + # Resolve the vhosts the easy way + networking.hosts."127.0.0.1" = [ + "proxied.${domain}" + "certchange.${domain}" + "zeroconf.${domain}" + "zeroconf2.${domain}" + "nullroot.${domain}" + ]; + + # OpenSSL will be used for more thorough certificate validation + environment.systemPackages = [ pkgs.openssl ]; + + # Used to determine if service reload was triggered. + # This does not provide a guarantee that the webserver is finished reloading, + # to handle that there is retry logic wrapping any connectivity checks. + systemd.targets."renew-triggered" = { + wantedBy = [ "${serverName}-config-reload.service" ]; + after = [ "${serverName}-config-reload.service" ]; + }; + + security.acme.certs."proxied.${domain}" = { + listenHTTP = ":8080"; + group = group; + }; + + specialisation = { + # Test that the web server is correctly reloaded when the cert changes + certchange.configuration = { + security.acme.certs."proxied.${domain}".extraDomainNames = [ + "certchange.${domain}" + ]; + }; + + # A useful transitional step before other tests, and tests behaviour + # of removing an extra domain from a cert. + certundo.configuration = { }; + + # Tests these features: + # - enableACME behaves as expected + # - serverAliases are appended to extraDomainNames + # - Correct routing to the specific virtualHost for a cert + # Inherits previous test config + zeroconf.configuration = { + services.${serverName}.virtualHosts."zeroconf.${domain}" = { + addSSL = true; + enableACME = true; + serverAliases = [ "zeroconf2.${domain}" ]; + }; + }; + + # Test that serverAliases are correctly removed which triggers + # cert regeneration and service reload. + rmalias.configuration = { + services.${serverName}.virtualHosts."zeroconf.${domain}" = { + addSSL = true; + enableACME = true; + }; + }; + + # Test that "acmeRoot = null" still results in + # valid cert generation by inheriting defaults. + nullroot.configuration = { + security.acme.defaults.listenHTTP = ":8080"; + services.${serverName}.virtualHosts."nullroot.${domain}" = { + onlySSL = true; + enableACME = true; + acmeRoot = null; + }; + }; + }; + }; + }; + + testScript = + { nodes, ... }: + '' + ${(import ./utils.nix).pythonUtils} + + domain = "${domain}" + ca_domain = "${nodes.acme.test-support.acme.caDomain}" + fqdn = f"proxied.{domain}" + + 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) + + with subtest("security.acme changes reflect on web server part 1"): + check_connection(webserver, f"certchange.{domain}", fail=True) + switch_to(webserver, "certchange") + webserver.wait_for_unit("renew-triggered.target") + check_connection(webserver, f"certchange.{domain}") + check_connection(webserver, fqdn) + + with subtest("security.acme changes reflect on web server part 2"): + check_connection(webserver, f"certchange.{domain}") + switch_to(webserver, "certundo") + webserver.wait_for_unit("renew-triggered.target") + check_connection(webserver, f"certchange.{domain}", fail=True) + check_connection(webserver, fqdn) + + with subtest("Zero configuration SSL certificates for a vhost"): + check_connection(webserver, f"zeroconf.{domain}", fail=True) + switch_to(webserver, "zeroconf") + webserver.wait_for_unit("renew-triggered.target") + check_connection(webserver, f"zeroconf.{domain}") + check_connection(webserver, f"zeroconf2.{domain}") + check_connection(webserver, fqdn) + + with subtest("Removing an alias from a vhost"): + check_connection(webserver, f"zeroconf2.{domain}") + switch_to(webserver, "rmalias") + webserver.wait_for_unit("renew-triggered.target") + check_connection(webserver, f"zeroconf2.{domain}", fail=True) + check_connection(webserver, f"zeroconf.{domain}") + check_connection(webserver, fqdn) + + with subtest("Create cert using inherited default validation mechanism"): + check_connection(webserver, f"nullroot.{domain}", fail=True) + switch_to(webserver, "nullroot") + webserver.wait_for_unit("renew-triggered.target") + check_connection(webserver, f"nullroot.{domain}") + ''; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index dcb0f1fe0a27..6eb63bf69147 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -105,7 +105,7 @@ in { _3proxy = runTest ./3proxy.nix; aaaaxy = runTest ./aaaaxy.nix; - acme = runTest ./acme.nix; + acme = import ./acme/default.nix { inherit runTest; }; acme-dns = handleTest ./acme-dns.nix {}; actual = handleTest ./actual.nix {}; adguardhome = runTest ./adguardhome.nix; diff --git a/nixos/tests/common/acme/server/default.nix b/nixos/tests/common/acme/server/default.nix index aaf6a8bd7348..1f4b57dc5851 100644 --- a/nixos/tests/common/acme/server/default.nix +++ b/nixos/tests/common/acme/server/default.nix @@ -6,53 +6,15 @@ # the test certificate into security.pki.certificateFiles or into package # overlays. # -# Another value that's needed if you don't use a custom resolver (see below for -# notes on that) is to add the acme node as a nameserver to every node -# that needs to acquire certificates using ACME, because otherwise the API host -# for acme.test can't be resolved. -# -# A configuration example of a full node setup using this would be this: -# -# { -# acme = import ./common/acme/server; -# -# example = { nodes, ... }: { -# networking.nameservers = [ -# nodes.acme.networking.primaryIPAddress -# ]; -# security.pki.certificateFiles = [ -# nodes.acme.test-support.acme.caCert -# ]; -# }; -# } -# -# By default, this module runs a local resolver, generated using resolver.nix -# from the parent directory to automatically discover all zones in the network. -# -# If you do not want this and want to use your own resolver, you can just -# override networking.nameservers like this: -# -# { -# acme = { nodes, lib, ... }: { -# imports = [ ./common/acme/server ]; -# networking.nameservers = lib.mkForce [ -# nodes.myresolver.networking.primaryIPAddress -# ]; -# }; -# -# myresolver = ...; -# } -# -# Keep in mind, that currently only _one_ resolver is supported, if you have -# more than one resolver in networking.nameservers only the first one will be -# used. -# -# Also make sure that whenever you use a resolver from a different test node -# that it has to be started _before_ the ACME service. +# The hosts file of this node will be populated with a mapping of certificate +# domains (including extraDomainNames) to their parent nodes in the test suite. +# This negates the need for a DNS server for most testing. You can still specify +# a custom nameserver/resolver if necessary for other reasons. { config, pkgs, lib, + nodes ? { }, ... }: let @@ -75,8 +37,6 @@ let in { - imports = [ ../../resolver.nix ]; - options.test-support.acme = { caDomain = lib.mkOption { type = lib.types.str; @@ -100,29 +60,49 @@ in }; config = { - test-support = { - resolver.enable = + networking = { + firewall.allowedTCPPorts = [ + 80 + 443 + 15000 + 4002 + ]; + + # Match the caDomain - nixos/lib/testing/network.nix will then add a record for us to + # all nodes in /etc/hosts + hostName = "acme"; + domain = "test"; + + # Extend /etc/hosts to resolve all configured certificates to their hosts. + # This way, no DNS server will be needed to validate HTTP-01 certs. + hosts = lib.attrsets.concatMapAttrs ( + _: node: let - isLocalResolver = config.networking.nameservers == [ "127.0.0.1" ]; + inherit (node.networking) primaryIPAddress primaryIPv6Address; + ips = builtins.filter (ip: ip != "") [ + primaryIPAddress + primaryIPv6Address + ]; + names = lib.lists.unique ( + lib.lists.flatten ( + lib.lists.concatMap + ( + cfg: + lib.attrsets.mapAttrsToList ( + domain: cfg: + builtins.map (builtins.replaceStrings [ "*." ] [ "" ]) ([ domain ] ++ cfg.extraDomainNames) + ) cfg.configuration.security.acme.certs + ) + # A specialisation's config is nested under its configuration attribute. + # For ease of use, nest the root node's configuration simiarly. + ([ { configuration = node; } ] ++ (builtins.attrValues node.specialisation)) + ) + ); in - lib.mkOverride 900 isLocalResolver; + builtins.listToAttrs (builtins.map (ip: lib.attrsets.nameValuePair ip names) ips) + ) nodes; }; - # This has priority 140, because modules/testing/test-instrumentation.nix - # already overrides this with priority 150. - networking.nameservers = lib.mkOverride 140 [ "127.0.0.1" ]; - networking.firewall.allowedTCPPorts = [ - 80 - 443 - 15000 - 4002 - ]; - - networking.extraHosts = '' - 127.0.0.1 ${domain} - ${config.networking.primaryIPAddress} ${domain} - ''; - systemd.services = { pebble = { enable = true; @@ -130,8 +110,9 @@ in wantedBy = [ "network.target" ]; environment = { # We're not testing lego, we're just testing our configuration. - # No need to sleep. + # No need to sleep or randomly fail nonces. PEBBLE_VA_NOSLEEP = "1"; + PEBBLE_WFE_NONCEREJECT = "0"; }; serviceConfig = { diff --git a/nixos/tests/common/resolver.nix b/nixos/tests/common/resolver.nix deleted file mode 100644 index 76d720f1fb85..000000000000 --- a/nixos/tests/common/resolver.nix +++ /dev/null @@ -1,184 +0,0 @@ -# This module automatically discovers zones in BIND and NSD NixOS -# configurations and creates zones for all definitions of networking.extraHosts -# (except those that point to 127.0.0.1 or ::1) within the current test network -# and delegates these zones using a fake root zone served by a BIND recursive -# name server. -{ - config, - nodes, - pkgs, - lib, - ... -}: - -{ - options.test-support.resolver.enable = lib.mkOption { - type = lib.types.bool; - default = true; - internal = true; - description = '' - Whether to enable the resolver that automatically discovers zone in the - test network. - - This option is `true` by default, because the module - defining this option needs to be explicitly imported. - - The reason this option exists is for the - {file}`nixos/tests/common/acme/server` module, which - needs that option to disable the resolver once the user has set its own - resolver. - ''; - }; - - config = lib.mkIf config.test-support.resolver.enable { - networking.firewall.enable = false; - services.bind.enable = true; - services.bind.cacheNetworks = lib.mkForce [ "any" ]; - services.bind.forwarders = lib.mkForce [ ]; - services.bind.zones = lib.singleton { - name = "."; - master = true; - file = - let - addDot = zone: zone + lib.optionalString (!lib.hasSuffix "." zone) "."; - mkNsdZoneNames = zones: map addDot (lib.attrNames zones); - mkBindZoneNames = zones: map addDot (lib.attrNames zones); - getZones = cfg: mkNsdZoneNames cfg.services.nsd.zones ++ mkBindZoneNames cfg.services.bind.zones; - - getZonesForNode = attrs: { - ip = attrs.config.networking.primaryIPAddress; - zones = lib.filter (zone: zone != ".") (getZones attrs.config); - }; - - zoneInfo = lib.mapAttrsToList (lib.const getZonesForNode) nodes; - - # A and AAAA resource records for all the definitions of - # networking.extraHosts except those for 127.0.0.1 or ::1. - # - # The result is an attribute set with keys being the host name and the - # values are either { ipv4 = ADDR; } or { ipv6 = ADDR; } where ADDR is - # the IP address for the corresponding key. - recordsFromExtraHosts = - let - getHostsForNode = lib.const (n: n.config.networking.extraHosts); - allHostsList = lib.mapAttrsToList getHostsForNode nodes; - allHosts = lib.concatStringsSep "\n" allHostsList; - - reIp = "[a-fA-F0-9.:]+"; - reHost = "[a-zA-Z0-9.-]+"; - - matchAliases = - str: - let - matched = builtins.match "[ \t]+(${reHost})(.*)" str; - continue = lib.singleton (lib.head matched) ++ matchAliases (lib.last matched); - in - lib.optional (matched != null) continue; - - matchLine = - str: - let - result = builtins.match "[ \t]*(${reIp})[ \t]+(${reHost})(.*)" str; - in - if result == null then - null - else - { - ipAddr = lib.head result; - hosts = lib.singleton (lib.elemAt result 1) ++ matchAliases (lib.last result); - }; - - skipLine = - str: - let - rest = builtins.match "[^\n]*\n(.*)" str; - in - if rest == null then "" else lib.head rest; - - getEntries = - str: acc: - let - result = matchLine str; - next = getEntries (skipLine str); - newEntry = acc ++ lib.singleton result; - continue = if result == null then next acc else next newEntry; - in - if str == "" then acc else continue; - - isIPv6 = str: builtins.match ".*:.*" str != null; - loopbackIps = [ - "127.0.0.1" - "::1" - ]; - filterLoopback = lib.filter (e: !lib.elem e.ipAddr loopbackIps); - - allEntries = lib.concatMap ( - entry: - map (host: { - inherit host; - ${if isIPv6 entry.ipAddr then "ipv6" else "ipv4"} = entry.ipAddr; - }) entry.hosts - ) (filterLoopback (getEntries (allHosts + "\n") [ ])); - - mkRecords = - entry: - let - records = - lib.optional (entry ? ipv6) "AAAA ${entry.ipv6}" - ++ lib.optional (entry ? ipv4) "A ${entry.ipv4}"; - mkRecord = typeAndData: "${entry.host}. IN ${typeAndData}"; - in - lib.concatMapStringsSep "\n" mkRecord records; - - in - lib.concatMapStringsSep "\n" mkRecords allEntries; - - # All of the zones that are subdomains of existing zones. - # For example if there is only "example.com" the following zones would - # be 'subZones': - # - # * foo.example.com. - # * bar.example.com. - # - # While the following would *not* be 'subZones': - # - # * example.com. - # * com. - # - subZones = - let - allZones = lib.concatMap (zi: zi.zones) zoneInfo; - isSubZoneOf = z1: z2: lib.hasSuffix z2 z1 && z1 != z2; - in - lib.filter (z: lib.any (isSubZoneOf z) allZones) allZones; - - # All the zones without 'subZones'. - filteredZoneInfo = map ( - zi: - zi - // { - zones = lib.filter (x: !lib.elem x subZones) zi.zones; - } - ) zoneInfo; - - in - pkgs.writeText "fake-root.zone" '' - $TTL 3600 - . IN SOA ns.fakedns. admin.fakedns. ( 1 3h 1h 1w 1d ) - ns.fakedns. IN A ${config.networking.primaryIPAddress} - . IN NS ns.fakedns. - ${lib.concatImapStrings ( - num: - { ip, zones }: - '' - ns${toString num}.fakedns. IN A ${ip} - ${lib.concatMapStrings (zone: '' - ${zone} IN NS ns${toString num}.fakedns. - '') zones} - '' - ) (lib.filter (zi: zi.zones != [ ]) filteredZoneInfo)} - ${recordsFromExtraHosts} - ''; - }; - }; -} diff --git a/pkgs/by-name/ca/caddy/package.nix b/pkgs/by-name/ca/caddy/package.nix index daf5c824799f..e867d5d0903c 100644 --- a/pkgs/by-name/ca/caddy/package.nix +++ b/pkgs/by-name/ca/caddy/package.nix @@ -77,6 +77,7 @@ buildGoModule { command = "${caddy}/bin/caddy version"; package = caddy; }; + acme-integration = nixosTests.acme.caddy; }; withPlugins = callPackage ./plugins.nix { inherit caddy; }; }; diff --git a/pkgs/by-name/le/lego/package.nix b/pkgs/by-name/le/lego/package.nix index 10059ac5bd0b..4b3acbabc391 100644 --- a/pkgs/by-name/le/lego/package.nix +++ b/pkgs/by-name/le/lego/package.nix @@ -29,5 +29,8 @@ buildGoModule rec { mainProgram = "lego"; }; - passthru.tests.lego = nixosTests.acme; + passthru.tests = { + lego-http = nixosTests.acme.http01-builtin; + lego-dns = nixosTests.acme.dns01-builtin; + }; } diff --git a/pkgs/by-name/pe/pebble/package.nix b/pkgs/by-name/pe/pebble/package.nix index b634fd0f400c..3c507c4daee7 100644 --- a/pkgs/by-name/pe/pebble/package.nix +++ b/pkgs/by-name/pe/pebble/package.nix @@ -25,7 +25,8 @@ buildGoModule rec { ]; passthru.tests = { - smoke-test = nixosTests.acme; + smoke-test-http = nixosTests.acme.http01-builtin; + smoke-test-dns = nixosTests.acme.dns01; }; meta = { diff --git a/pkgs/servers/http/apache-httpd/2.4.nix b/pkgs/servers/http/apache-httpd/2.4.nix index 0eb9ba467163..fc4c6f3e136e 100644 --- a/pkgs/servers/http/apache-httpd/2.4.nix +++ b/pkgs/servers/http/apache-httpd/2.4.nix @@ -99,7 +99,7 @@ stdenv.mkDerivation rec { passthru = { inherit apr aprutil sslSupport proxySupport ldapSupport luaSupport lua5; tests = { - acme-integration = nixosTests.acme; + acme-integration = nixosTests.acme.httpd; proxy = nixosTests.proxy; php = nixosTests.php.httpd; cross = runCommand "apacheHttpd-test-cross" { } '' diff --git a/pkgs/servers/http/nginx/generic.nix b/pkgs/servers/http/nginx/generic.nix index a767cab27d11..9c5feef4be0b 100644 --- a/pkgs/servers/http/nginx/generic.nix +++ b/pkgs/servers/http/nginx/generic.nix @@ -300,7 +300,7 @@ stdenv.mkDerivation { nginx-unix-socket ; variants = lib.recurseIntoAttrs nixosTests.nginx-variants; - acme-integration = nixosTests.acme; + acme-integration = nixosTests.acme.nginx; } // passthru.tests; };