diff options
author | Alyssa Ross <hi@alyssa.is> | 2023-06-16 06:56:35 +0000 |
---|---|---|
committer | Alyssa Ross <hi@alyssa.is> | 2023-06-16 06:56:35 +0000 |
commit | 99fcaeccb89621dd492203ce1f2d551c06f228ed (patch) | |
tree | 41cb730ae07383004789779b0f6e11cb3f4642a3 /nixpkgs/nixos/tests/acme.nix | |
parent | 59c5f5ac8682acc13bb22bc29c7cf02f7d75f01f (diff) | |
parent | 75a5ebf473cd60148ba9aec0d219f72e5cf52519 (diff) | |
download | nixlib-99fcaeccb89621dd492203ce1f2d551c06f228ed.tar nixlib-99fcaeccb89621dd492203ce1f2d551c06f228ed.tar.gz nixlib-99fcaeccb89621dd492203ce1f2d551c06f228ed.tar.bz2 nixlib-99fcaeccb89621dd492203ce1f2d551c06f228ed.tar.lz nixlib-99fcaeccb89621dd492203ce1f2d551c06f228ed.tar.xz nixlib-99fcaeccb89621dd492203ce1f2d551c06f228ed.tar.zst nixlib-99fcaeccb89621dd492203ce1f2d551c06f228ed.zip |
Merge branch 'nixos-unstable' of https://github.com/NixOS/nixpkgs
Conflicts: nixpkgs/nixos/modules/config/console.nix nixpkgs/nixos/modules/services/mail/mailman.nix nixpkgs/nixos/modules/services/mail/public-inbox.nix nixpkgs/nixos/modules/services/mail/rss2email.nix nixpkgs/nixos/modules/services/networking/ssh/sshd.nix nixpkgs/pkgs/applications/networking/instant-messengers/dino/default.nix nixpkgs/pkgs/applications/networking/irc/weechat/default.nix nixpkgs/pkgs/applications/window-managers/sway/default.nix nixpkgs/pkgs/build-support/go/module.nix nixpkgs/pkgs/build-support/rust/build-rust-package/default.nix nixpkgs/pkgs/development/interpreters/python/default.nix nixpkgs/pkgs/development/node-packages/overrides.nix nixpkgs/pkgs/development/tools/b4/default.nix nixpkgs/pkgs/servers/dict/dictd-db.nix nixpkgs/pkgs/servers/mail/public-inbox/default.nix nixpkgs/pkgs/tools/security/pinentry/default.nix nixpkgs/pkgs/tools/text/unoconv/default.nix nixpkgs/pkgs/top-level/all-packages.nix
Diffstat (limited to 'nixpkgs/nixos/tests/acme.nix')
-rw-r--r-- | nixpkgs/nixos/tests/acme.nix | 140 |
1 files changed, 109 insertions, 31 deletions
diff --git a/nixpkgs/nixos/tests/acme.nix b/nixpkgs/nixos/tests/acme.nix index c07f99c5db3a..4d220b9747aa 100644 --- a/nixpkgs/nixos/tests/acme.nix +++ b/nixpkgs/nixos/tests/acme.nix @@ -1,7 +1,7 @@ -import ./make-test-python.nix ({ pkgs, lib, ... }: let +{ pkgs, lib, ... }: let commonConfig = ./common/acme/client; - dnsServerIP = nodes: nodes.dnsserver.config.networking.primaryIPAddress; + dnsServerIP = nodes: nodes.dnsserver.networking.primaryIPAddress; dnsScript = nodes: let dnsAddress = dnsServerIP nodes; @@ -41,6 +41,16 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: let 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; @@ -134,7 +144,11 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: let in { name = "acme"; - meta.maintainers = lib.teams.acme.members; + 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 @@ -153,7 +167,7 @@ in { description = "Pebble ACME challenge test server"; wantedBy = [ "network.target" ]; serviceConfig = { - ExecStart = "${pkgs.pebble}/bin/pebble-challtestsrv -dns01 ':53' -defaultIPv6 '' -defaultIPv4 '${nodes.webserver.config.networking.primaryIPAddress}'"; + 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" ]; }; @@ -173,9 +187,29 @@ in { services.nginx.logError = "stderr info"; specialisation = { + # Tests HTTP-01 verification using Lego's built-in web server + http01lego.configuration = simpleConfig; + + 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.config.test-support.acme.caDomain; + 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 = '' @@ -247,7 +281,7 @@ in { }; }; - # Test compatiblity with Caddy + # Test compatibility with Caddy # It only supports useACMEHost, hence not using mkServerConfigs } // (let baseCaddyConfig = { nodes, config, ... }: { @@ -316,7 +350,7 @@ in { testScript = { nodes, ... }: let - caDomain = nodes.acme.config.test-support.acme.caDomain; + caDomain = nodes.acme.test-support.acme.caDomain; newServerSystem = nodes.webserver.config.system.build.toplevel; switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test"; in @@ -327,6 +361,30 @@ in { 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 + + + backoff = BackoffTracker() + + def switch_to(node, name): # On first switch, this will create a symlink to the current system so that we can # quickly switch between derivations @@ -349,7 +407,7 @@ in { # 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 afer verification + # are not still selfsigned after verification def check_issuer(node, cert_name, issuer): for fname in ("cert.pem", "fullchain.pem"): actual_issuer = node.succeed( @@ -374,9 +432,7 @@ in { assert False - def check_connection(node, domain, retries=3): - assert retries >= 0, f"Failed to connect to https://{domain}" - + def check_connection(node, domain, retries=0): result = node.succeed( "openssl s_client -brief -verify 2 -CAfile /tmp/ca.crt" f" -servername {domain} -connect {domain}:443 < /dev/null 2>&1" @@ -384,13 +440,11 @@ in { for line in result.lower().split("\n"): if "verification" in line and "error" in line: - time.sleep(3) - return check_connection(node, domain, retries - 1) + retries = backoff.handle_fail(retries, f"Failed to connect to https://{domain}") + return check_connection(node, domain, retries) - def check_connection_key_bits(node, domain, bits, retries=3): - assert retries >= 0, f"Did not find expected number of bits ({bits}) in key" - + def check_connection_key_bits(node, domain, bits, retries=0): result = node.succeed( "openssl s_client -CAfile /tmp/ca.crt" f" -servername {domain} -connect {domain}:443 < /dev/null" @@ -399,13 +453,11 @@ in { print("Key type:", result) if bits not in result: - time.sleep(3) - return check_connection_key_bits(node, domain, bits, retries - 1) - + retries = backoff.handle_fail(retries, f"Did not find expected number of bits ({bits}) in key") + return check_connection_key_bits(node, domain, bits, retries) - def check_stapling(node, domain, retries=3): - assert retries >= 0, "OCSP Stapling check failed" + def check_stapling(node, domain, retries=0): # Pebble doesn't provide a full OCSP responder, so just check the URL result = node.succeed( "openssl s_client -CAfile /tmp/ca.crt" @@ -415,21 +467,19 @@ in { print("OCSP Responder URL:", result) if "${caDomain}:4002" not in result.lower(): - time.sleep(3) - return check_stapling(node, domain, retries - 1) - + retries = backoff.handle_fail(retries, "OCSP Stapling check failed") + return check_stapling(node, domain, retries) - def download_ca_certs(node, retries=5): - assert retries >= 0, "Failed to connect to pebble to download root CA certs" + def download_ca_certs(node, retries=0): exit_code, _ = node.execute("curl https://${caDomain}:15000/roots/0 > /tmp/ca.crt") exit_code_2, _ = node.execute( "curl https://${caDomain}:15000/intermediate-keys/0 >> /tmp/ca.crt" ) if exit_code + exit_code_2 > 0: - time.sleep(3) - return download_ca_certs(node, retries - 1) + retries = backoff.handle_fail(retries, "Failed to connect to pebble to download root CA certs") + return download_ca_certs(node, retries) start_all() @@ -438,7 +488,7 @@ in { client.wait_for_unit("default.target") client.succeed( - 'curl --data \'{"host": "${caDomain}", "addresses": ["${nodes.acme.config.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a' + 'curl --data \'{"host": "${caDomain}", "addresses": ["${nodes.acme.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a' ) acme.wait_for_unit("network-online.target") @@ -446,7 +496,35 @@ in { download_ca_certs(client) - # Perform general tests first + # Perform http-01 w/ lego test first + with subtest("Can request certificate with Lego's built in web server"): + switch_to(webserver, "http01lego") + webserver.wait_for_unit("acme-finished-http.example.test.target") + check_fullchain(webserver, "http.example.test") + check_issuer(webserver, "http.example.test", "pebble") + + # 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") + webserver.wait_for_unit("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") + webserver.wait_for_unit("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"): @@ -594,4 +672,4 @@ in { wait_for_server() check_connection_key_bits(client, test_domain, "384") ''; -}) +} |