at 24.11-pre 29 kB view raw
1{ config, lib, ... }: let 2 3 pkgs = config.node.pkgs; 4 5 commonConfig = ./common/acme/client; 6 7 dnsServerIP = nodes: nodes.dnsserver.networking.primaryIPAddress; 8 9 dnsScript = nodes: let 10 dnsAddress = dnsServerIP nodes; 11 in pkgs.writeShellScript "dns-hook.sh" '' 12 set -euo pipefail 13 echo '[INFO]' "[$2]" 'dns-hook.sh' $* 14 if [ "$1" = "present" ]; then 15 ${pkgs.curl}/bin/curl --data '{"host": "'"$2"'", "value": "'"$3"'"}' http://${dnsAddress}:8055/set-txt 16 else 17 ${pkgs.curl}/bin/curl --data '{"host": "'"$2"'"}' http://${dnsAddress}:8055/clear-txt 18 fi 19 ''; 20 21 dnsConfig = nodes: { 22 dnsProvider = "exec"; 23 dnsPropagationCheck = false; 24 environmentFile = pkgs.writeText "wildcard.env" '' 25 EXEC_PATH=${dnsScript nodes} 26 EXEC_POLLING_INTERVAL=1 27 EXEC_PROPAGATION_TIMEOUT=1 28 EXEC_SEQUENCE_INTERVAL=1 29 ''; 30 }; 31 32 documentRoot = pkgs.runCommand "docroot" {} '' 33 mkdir -p "$out" 34 echo hello world > "$out/index.html" 35 ''; 36 37 vhostBase = { 38 forceSSL = true; 39 locations."/".root = documentRoot; 40 }; 41 42 vhostBaseHttpd = { 43 forceSSL = true; 44 inherit documentRoot; 45 }; 46 47 simpleConfig = { 48 security.acme = { 49 certs."http.example.test" = { 50 listenHTTP = ":80"; 51 }; 52 }; 53 54 networking.firewall.allowedTCPPorts = [ 80 ]; 55 }; 56 57 # Base specialisation config for testing general ACME features 58 webserverBasicConfig = { 59 services.nginx.enable = true; 60 services.nginx.virtualHosts."a.example.test" = vhostBase // { 61 enableACME = true; 62 }; 63 }; 64 65 # Generate specialisations for testing a web server 66 mkServerConfigs = { server, group, vhostBaseData, extraConfig ? {} }: let 67 baseConfig = { nodes, config, specialConfig ? {} }: lib.mkMerge [ 68 { 69 security.acme = { 70 defaults = (dnsConfig nodes); 71 # One manual wildcard cert 72 certs."example.test" = { 73 domain = "*.example.test"; 74 }; 75 }; 76 77 users.users."${config.services."${server}".user}".extraGroups = ["acme"]; 78 79 services."${server}" = { 80 enable = true; 81 virtualHosts = { 82 # Run-of-the-mill vhost using HTTP-01 validation 83 "${server}-http.example.test" = vhostBaseData // { 84 serverAliases = [ "${server}-http-alias.example.test" ]; 85 enableACME = true; 86 }; 87 88 # Another which inherits the DNS-01 config 89 "${server}-dns.example.test" = vhostBaseData // { 90 serverAliases = [ "${server}-dns-alias.example.test" ]; 91 enableACME = true; 92 # Set acmeRoot to null instead of using the default of "/var/lib/acme/acme-challenge" 93 # webroot + dnsProvider are mutually exclusive. 94 acmeRoot = null; 95 }; 96 97 # One using the wildcard certificate 98 "${server}-wildcard.example.test" = vhostBaseData // { 99 serverAliases = [ "${server}-wildcard-alias.example.test" ]; 100 useACMEHost = "example.test"; 101 }; 102 } // (lib.optionalAttrs (server == "nginx") { 103 # The nginx module supports using a different key than the hostname 104 different-key = vhostBaseData // { 105 serverName = "${server}-different-key.example.test"; 106 serverAliases = [ "${server}-different-key-alias.example.test" ]; 107 enableACME = true; 108 }; 109 }); 110 }; 111 112 # Used to determine if service reload was triggered 113 systemd.targets."test-renew-${server}" = { 114 wants = [ "acme-${server}-http.example.test.service" ]; 115 after = [ "acme-${server}-http.example.test.service" "${server}-config-reload.service" ]; 116 }; 117 } 118 specialConfig 119 extraConfig 120 ]; 121 in { 122 "${server}".configuration = { nodes, config, ... }: baseConfig { 123 inherit nodes config; 124 }; 125 126 # Test that server reloads when an alias is removed (and subsequently test removal works in acme) 127 "${server}-remove-alias".configuration = { nodes, config, ... }: baseConfig { 128 inherit nodes config; 129 specialConfig = { 130 # Remove an alias, but create a standalone vhost in its place for testing. 131 # This configuration results in certificate errors as useACMEHost does not imply 132 # append extraDomains, and thus we can validate the SAN is removed. 133 services."${server}" = { 134 virtualHosts."${server}-http.example.test".serverAliases = lib.mkForce []; 135 virtualHosts."${server}-http-alias.example.test" = vhostBaseData // { 136 useACMEHost = "${server}-http.example.test"; 137 }; 138 }; 139 }; 140 }; 141 142 # Test that the server reloads when only the acme configuration is changed. 143 "${server}-change-acme-conf".configuration = { nodes, config, ... }: baseConfig { 144 inherit nodes config; 145 specialConfig = { 146 security.acme.certs."${server}-http.example.test" = { 147 keyType = "ec384"; 148 # Also test that postRun is exec'd as root 149 postRun = "id | grep root"; 150 }; 151 }; 152 }; 153 }; 154 155in { 156 name = "acme"; 157 meta = { 158 maintainers = lib.teams.acme.members; 159 # Hard timeout in seconds. Average run time is about 7 minutes. 160 timeout = 1800; 161 }; 162 163 nodes = { 164 # The fake ACME server which will respond to client requests 165 acme = { nodes, ... }: { 166 imports = [ ./common/acme/server ]; 167 networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ]; 168 }; 169 170 # A fake DNS server which can be configured with records as desired 171 # Used to test DNS-01 challenge 172 dnsserver = { nodes, ... }: { 173 networking.firewall.allowedTCPPorts = [ 8055 53 ]; 174 networking.firewall.allowedUDPPorts = [ 53 ]; 175 systemd.services.pebble-challtestsrv = { 176 enable = true; 177 description = "Pebble ACME challenge test server"; 178 wantedBy = [ "network.target" ]; 179 serviceConfig = { 180 ExecStart = "${pkgs.pebble}/bin/pebble-challtestsrv -dns01 ':53' -defaultIPv6 '' -defaultIPv4 '${nodes.webserver.networking.primaryIPAddress}'"; 181 # Required to bind on privileged ports. 182 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 183 }; 184 }; 185 }; 186 187 # A web server which will be the node requesting certs 188 webserver = { nodes, config, ... }: { 189 imports = [ commonConfig ]; 190 networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ]; 191 networking.firewall.allowedTCPPorts = [ 80 443 ]; 192 193 # OpenSSL will be used for more thorough certificate validation 194 environment.systemPackages = [ pkgs.openssl ]; 195 196 # Set log level to info so that we can see when the service is reloaded 197 services.nginx.logError = "stderr info"; 198 199 specialisation = { 200 # Tests HTTP-01 verification using Lego's built-in web server 201 http01lego.configuration = simpleConfig; 202 203 renew.configuration = lib.mkMerge [ 204 simpleConfig 205 { 206 # Pebble provides 5 year long certs, 207 # needs to be higher than that to test renewal 208 security.acme.certs."http.example.test".validMinDays = 9999; 209 } 210 ]; 211 212 # Tests that account creds can be safely changed. 213 accountchange.configuration = lib.mkMerge [ 214 simpleConfig 215 { 216 security.acme.certs."http.example.test".email = "admin@example.test"; 217 } 218 ]; 219 220 # First derivation used to test general ACME features 221 general.configuration = { ... }: let 222 caDomain = nodes.acme.test-support.acme.caDomain; 223 email = config.security.acme.defaults.email; 224 # Exit 99 to make it easier to track if this is the reason a renew failed 225 accountCreateTester = '' 226 test -e accounts/${caDomain}/${email}/account.json || exit 99 227 ''; 228 in lib.mkMerge [ 229 webserverBasicConfig 230 { 231 # Used to test that account creation is collated into one service. 232 # These should not run until after acme-finished-a.example.test.target 233 systemd.services."b.example.test".preStart = accountCreateTester; 234 systemd.services."c.example.test".preStart = accountCreateTester; 235 236 services.nginx.virtualHosts."b.example.test" = vhostBase // { 237 enableACME = true; 238 }; 239 services.nginx.virtualHosts."c.example.test" = vhostBase // { 240 enableACME = true; 241 }; 242 } 243 ]; 244 245 # Test OCSP Stapling 246 ocsp-stapling.configuration = { ... }: lib.mkMerge [ 247 webserverBasicConfig 248 { 249 security.acme.certs."a.example.test".ocspMustStaple = true; 250 services.nginx.virtualHosts."a.example.test" = { 251 extraConfig = '' 252 ssl_stapling on; 253 ssl_stapling_verify on; 254 ''; 255 }; 256 } 257 ]; 258 259 # Validate service relationships by adding a slow start service to nginx' wants. 260 # Reproducer for https://github.com/NixOS/nixpkgs/issues/81842 261 slow-startup.configuration = { ... }: lib.mkMerge [ 262 webserverBasicConfig 263 { 264 systemd.services.my-slow-service = { 265 wantedBy = [ "multi-user.target" "nginx.service" ]; 266 before = [ "nginx.service" ]; 267 preStart = "sleep 5"; 268 script = "${pkgs.python3}/bin/python -m http.server"; 269 }; 270 271 services.nginx.virtualHosts."slow.example.test" = { 272 forceSSL = true; 273 enableACME = true; 274 locations."/".proxyPass = "http://localhost:8000"; 275 }; 276 } 277 ]; 278 279 concurrency-limit.configuration = {pkgs, ...}: lib.mkMerge [ 280 webserverBasicConfig { 281 security.acme.maxConcurrentRenewals = 1; 282 283 services.nginx.virtualHosts = { 284 "f.example.test" = vhostBase // { 285 enableACME = true; 286 }; 287 "g.example.test" = vhostBase // { 288 enableACME = true; 289 }; 290 "h.example.test" = vhostBase // { 291 enableACME = true; 292 }; 293 }; 294 295 systemd.services = { 296 # check for mutual exclusion of starting renew services 297 "acme-f.example.test".serviceConfig.ExecPreStart = "+" + (pkgs.writeShellScript "test-f" '' 298 test "$(systemctl is-active acme-{g,h}.example.test.service | grep activating | wc -l)" -le 0 299 ''); 300 "acme-g.example.test".serviceConfig.ExecPreStart = "+" + (pkgs.writeShellScript "test-g" '' 301 test "$(systemctl is-active acme-{f,h}.example.test.service | grep activating | wc -l)" -le 0 302 ''); 303 "acme-h.example.test".serviceConfig.ExecPreStart = "+" + (pkgs.writeShellScript "test-h" '' 304 test "$(systemctl is-active acme-{g,f}.example.test.service | grep activating | wc -l)" -le 0 305 ''); 306 }; 307 } 308 ]; 309 310 # Test lego internal server (listenHTTP option) 311 # Also tests useRoot option 312 lego-server.configuration = { ... }: { 313 security.acme.useRoot = true; 314 security.acme.certs."lego.example.test" = { 315 listenHTTP = ":80"; 316 group = "nginx"; 317 }; 318 services.nginx.enable = true; 319 services.nginx.virtualHosts."lego.example.test" = { 320 useACMEHost = "lego.example.test"; 321 onlySSL = true; 322 }; 323 }; 324 325 # Test compatibility with Caddy 326 # It only supports useACMEHost, hence not using mkServerConfigs 327 } // (let 328 baseCaddyConfig = { nodes, config, ... }: { 329 security.acme = { 330 defaults = (dnsConfig nodes); 331 # One manual wildcard cert 332 certs."example.test" = { 333 domain = "*.example.test"; 334 }; 335 }; 336 337 users.users."${config.services.caddy.user}".extraGroups = ["acme"]; 338 339 services.caddy = { 340 enable = true; 341 virtualHosts."a.example.test" = { 342 useACMEHost = "example.test"; 343 extraConfig = '' 344 root * ${documentRoot} 345 ''; 346 }; 347 }; 348 }; 349 in { 350 caddy.configuration = baseCaddyConfig; 351 352 # Test that the server reloads when only the acme configuration is changed. 353 "caddy-change-acme-conf".configuration = { nodes, config, ... }: lib.mkMerge [ 354 (baseCaddyConfig { 355 inherit nodes config; 356 }) 357 { 358 security.acme.certs."example.test" = { 359 keyType = "ec384"; 360 }; 361 } 362 ]; 363 364 # Test compatibility with Nginx 365 }) // (mkServerConfigs { 366 server = "nginx"; 367 group = "nginx"; 368 vhostBaseData = vhostBase; 369 }) 370 371 # Test compatibility with Apache HTTPD 372 // (mkServerConfigs { 373 server = "httpd"; 374 group = "wwwrun"; 375 vhostBaseData = vhostBaseHttpd; 376 extraConfig = { 377 services.httpd.adminAddr = config.security.acme.defaults.email; 378 }; 379 }); 380 }; 381 382 # The client will be used to curl the webserver to validate configuration 383 client = { nodes, ... }: { 384 imports = [ commonConfig ]; 385 networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ]; 386 387 # OpenSSL will be used for more thorough certificate validation 388 environment.systemPackages = [ pkgs.openssl ]; 389 }; 390 }; 391 392 testScript = { nodes, ... }: 393 let 394 caDomain = nodes.acme.test-support.acme.caDomain; 395 newServerSystem = nodes.webserver.config.system.build.toplevel; 396 switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test"; 397 in 398 # Note, wait_for_unit does not work for oneshot services that do not have RemainAfterExit=true, 399 # this is because a oneshot goes from inactive => activating => inactive, and never 400 # reaches the active state. Targets do not have this issue. 401 '' 402 import time 403 404 405 TOTAL_RETRIES = 20 406 407 408 class BackoffTracker(object): 409 delay = 1 410 increment = 1 411 412 def handle_fail(self, retries, message) -> int: 413 assert retries < TOTAL_RETRIES, message 414 415 print(f"Retrying in {self.delay}s, {retries + 1}/{TOTAL_RETRIES}") 416 time.sleep(self.delay) 417 418 # Only increment after the first try 419 if retries == 0: 420 self.delay += self.increment 421 self.increment *= 2 422 423 return retries + 1 424 425 426 backoff = BackoffTracker() 427 428 429 def switch_to(node, name): 430 # On first switch, this will create a symlink to the current system so that we can 431 # quickly switch between derivations 432 root_specs = "/tmp/specialisation" 433 node.execute( 434 f"test -e {root_specs}" 435 f" || ln -s $(readlink /run/current-system)/specialisation {root_specs}" 436 ) 437 438 switcher_path = f"/run/current-system/specialisation/{name}/bin/switch-to-configuration" 439 rc, _ = node.execute(f"test -e '{switcher_path}'") 440 if rc > 0: 441 switcher_path = f"/tmp/specialisation/{name}/bin/switch-to-configuration" 442 443 node.succeed( 444 f"{switcher_path} test" 445 ) 446 447 448 # Ensures the issuer of our cert matches the chain 449 # and matches the issuer we expect it to be. 450 # It's a good validation to ensure the cert.pem and fullchain.pem 451 # are not still selfsigned after verification 452 def check_issuer(node, cert_name, issuer): 453 for fname in ("cert.pem", "fullchain.pem"): 454 actual_issuer = node.succeed( 455 f"openssl x509 -noout -issuer -in /var/lib/acme/{cert_name}/{fname}" 456 ).partition("=")[2] 457 print(f"{fname} issuer: {actual_issuer}") 458 assert issuer.lower() in actual_issuer.lower() 459 460 461 # Ensure cert comes before chain in fullchain.pem 462 def check_fullchain(node, cert_name): 463 subject_data = node.succeed( 464 f"openssl crl2pkcs7 -nocrl -certfile /var/lib/acme/{cert_name}/fullchain.pem" 465 " | openssl pkcs7 -print_certs -noout" 466 ) 467 for line in subject_data.lower().split("\n"): 468 if "subject" in line: 469 print(f"First subject in fullchain.pem: {line}") 470 assert cert_name.lower() in line 471 return 472 473 assert False 474 475 476 def check_connection(node, domain, retries=0): 477 result = node.succeed( 478 "openssl s_client -brief -verify 2 -CAfile /tmp/ca.crt" 479 f" -servername {domain} -connect {domain}:443 < /dev/null 2>&1" 480 ) 481 482 for line in result.lower().split("\n"): 483 if "verification" in line and "error" in line: 484 retries = backoff.handle_fail(retries, f"Failed to connect to https://{domain}") 485 return check_connection(node, domain, retries) 486 487 488 def check_connection_key_bits(node, domain, bits, retries=0): 489 result = node.succeed( 490 "openssl s_client -CAfile /tmp/ca.crt" 491 f" -servername {domain} -connect {domain}:443 < /dev/null" 492 " | openssl x509 -noout -text | grep -i Public-Key" 493 ) 494 print("Key type:", result) 495 496 if bits not in result: 497 retries = backoff.handle_fail(retries, f"Did not find expected number of bits ({bits}) in key") 498 return check_connection_key_bits(node, domain, bits, retries) 499 500 501 def check_stapling(node, domain, retries=0): 502 # Pebble doesn't provide a full OCSP responder, so just check the URL 503 result = node.succeed( 504 "openssl s_client -CAfile /tmp/ca.crt" 505 f" -servername {domain} -connect {domain}:443 < /dev/null" 506 " | openssl x509 -noout -ocsp_uri" 507 ) 508 print("OCSP Responder URL:", result) 509 510 if "${caDomain}:4002" not in result.lower(): 511 retries = backoff.handle_fail(retries, "OCSP Stapling check failed") 512 return check_stapling(node, domain, retries) 513 514 515 def download_ca_certs(node, retries=0): 516 exit_code, _ = node.execute("curl https://${caDomain}:15000/roots/0 > /tmp/ca.crt") 517 exit_code_2, _ = node.execute( 518 "curl https://${caDomain}:15000/intermediate-keys/0 >> /tmp/ca.crt" 519 ) 520 521 if exit_code + exit_code_2 > 0: 522 retries = backoff.handle_fail(retries, "Failed to connect to pebble to download root CA certs") 523 return download_ca_certs(node, retries) 524 525 526 start_all() 527 528 dnsserver.wait_for_unit("pebble-challtestsrv.service") 529 client.wait_for_unit("default.target") 530 531 client.succeed( 532 'curl --data \'{"host": "${caDomain}", "addresses": ["${nodes.acme.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a' 533 ) 534 535 acme.systemctl("start network-online.target") 536 acme.wait_for_unit("network-online.target") 537 acme.wait_for_unit("pebble.service") 538 539 download_ca_certs(client) 540 541 # Perform http-01 w/ lego test first 542 with subtest("Can request certificate with Lego's built in web server"): 543 switch_to(webserver, "http01lego") 544 webserver.wait_for_unit("acme-finished-http.example.test.target") 545 check_fullchain(webserver, "http.example.test") 546 check_issuer(webserver, "http.example.test", "pebble") 547 548 # Perform renewal test 549 with subtest("Can renew certificates when they expire"): 550 hash = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem") 551 switch_to(webserver, "renew") 552 webserver.wait_for_unit("acme-finished-http.example.test.target") 553 check_fullchain(webserver, "http.example.test") 554 check_issuer(webserver, "http.example.test", "pebble") 555 hash_after = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem") 556 assert hash != hash_after 557 558 # Perform account change test 559 with subtest("Handles email change correctly"): 560 hash = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem") 561 switch_to(webserver, "accountchange") 562 webserver.wait_for_unit("acme-finished-http.example.test.target") 563 check_fullchain(webserver, "http.example.test") 564 check_issuer(webserver, "http.example.test", "pebble") 565 hash_after = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem") 566 # Has to do a full run to register account, which creates new certs. 567 assert hash != hash_after 568 569 # Perform general tests 570 switch_to(webserver, "general") 571 572 with subtest("Can request certificate with HTTP-01 challenge"): 573 webserver.wait_for_unit("acme-finished-a.example.test.target") 574 check_fullchain(webserver, "a.example.test") 575 check_issuer(webserver, "a.example.test", "pebble") 576 webserver.wait_for_unit("nginx.service") 577 check_connection(client, "a.example.test") 578 579 with subtest("Runs 1 cert for account creation before others"): 580 webserver.wait_for_unit("acme-finished-b.example.test.target") 581 webserver.wait_for_unit("acme-finished-c.example.test.target") 582 check_connection(client, "b.example.test") 583 check_connection(client, "c.example.test") 584 585 with subtest("Certificates and accounts have safe + valid permissions"): 586 # Nginx will set the group appropriately when enableACME is used 587 group = "nginx" 588 webserver.succeed( 589 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" 590 ) 591 webserver.succeed( 592 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" 593 ) 594 webserver.succeed( 595 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" 596 ) 597 webserver.succeed( 598 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" 599 ) 600 601 # Selfsigned certs tests happen late so we aren't fighting the system init triggering cert renewal 602 with subtest("Can generate valid selfsigned certs"): 603 webserver.succeed("systemctl clean acme-a.example.test.service --what=state") 604 webserver.succeed("systemctl start acme-selfsigned-a.example.test.service") 605 check_fullchain(webserver, "a.example.test") 606 check_issuer(webserver, "a.example.test", "minica") 607 # Check selfsigned permissions 608 webserver.succeed( 609 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" 610 ) 611 # Will succeed if nginx can load the certs 612 webserver.succeed("systemctl start nginx-config-reload.service") 613 614 with subtest("Correctly implements OCSP stapling"): 615 switch_to(webserver, "ocsp-stapling") 616 webserver.wait_for_unit("acme-finished-a.example.test.target") 617 check_stapling(client, "a.example.test") 618 619 with subtest("Can request certificate with HTTP-01 using lego's internal web server"): 620 switch_to(webserver, "lego-server") 621 webserver.wait_for_unit("acme-finished-lego.example.test.target") 622 webserver.wait_for_unit("nginx.service") 623 webserver.succeed("echo HENLO && systemctl cat nginx.service") 624 webserver.succeed("test \"$(stat -c '%U' /var/lib/acme/* | uniq)\" = \"root\"") 625 check_connection(client, "a.example.test") 626 check_connection(client, "lego.example.test") 627 628 with subtest("Can request certificate with HTTP-01 when nginx startup is delayed"): 629 webserver.execute("systemctl stop nginx") 630 switch_to(webserver, "slow-startup") 631 webserver.wait_for_unit("acme-finished-slow.example.test.target") 632 check_issuer(webserver, "slow.example.test", "pebble") 633 webserver.wait_for_unit("nginx.service") 634 check_connection(client, "slow.example.test") 635 636 with subtest("Can limit concurrency of running renewals"): 637 switch_to(webserver, "concurrency-limit") 638 webserver.wait_for_unit("acme-finished-f.example.test.target") 639 webserver.wait_for_unit("acme-finished-g.example.test.target") 640 webserver.wait_for_unit("acme-finished-h.example.test.target") 641 check_connection(client, "f.example.test") 642 check_connection(client, "g.example.test") 643 check_connection(client, "h.example.test") 644 645 with subtest("Works with caddy"): 646 switch_to(webserver, "caddy") 647 webserver.wait_for_unit("acme-finished-example.test.target") 648 webserver.wait_for_unit("caddy.service") 649 # FIXME reloading caddy is not sufficient to load new certs. 650 # Restart it manually until this is fixed. 651 webserver.succeed("systemctl restart caddy.service") 652 check_connection(client, "a.example.test") 653 654 with subtest("security.acme changes reflect on caddy"): 655 switch_to(webserver, "caddy-change-acme-conf") 656 webserver.wait_for_unit("acme-finished-example.test.target") 657 webserver.wait_for_unit("caddy.service") 658 # FIXME reloading caddy is not sufficient to load new certs. 659 # Restart it manually until this is fixed. 660 webserver.succeed("systemctl restart caddy.service") 661 check_connection_key_bits(client, "a.example.test", "384") 662 663 common_domains = ["http", "dns", "wildcard"] 664 for server, logsrc, domains in [ 665 ("nginx", "journalctl -n 30 -u nginx.service", common_domains + ["different-key"]), 666 ("httpd", "tail -n 30 /var/log/httpd/*.log", common_domains), 667 ]: 668 wait_for_server = lambda: webserver.wait_for_unit(f"{server}.service") 669 with subtest(f"Works with {server}"): 670 try: 671 switch_to(webserver, server) 672 for domain in domains: 673 if domain != "wildcard": 674 webserver.wait_for_unit( 675 f"acme-finished-{server}-{domain}.example.test.target" 676 ) 677 except Exception as err: 678 _, output = webserver.execute( 679 f"{logsrc} && ls -al /var/lib/acme/acme-challenge" 680 ) 681 print(output) 682 raise err 683 684 wait_for_server() 685 686 for domain in domains: 687 if domain != "wildcard": 688 check_issuer(webserver, f"{server}-{domain}.example.test", "pebble") 689 for domain in domains: 690 check_connection(client, f"{server}-{domain}.example.test") 691 check_connection(client, f"{server}-{domain}-alias.example.test") 692 693 test_domain = f"{server}-{domains[0]}.example.test" 694 695 with subtest(f"Can reload {server} when timer triggers renewal"): 696 # Switch to selfsigned first 697 webserver.succeed(f"systemctl clean acme-{test_domain}.service --what=state") 698 webserver.succeed(f"systemctl start acme-selfsigned-{test_domain}.service") 699 check_issuer(webserver, test_domain, "minica") 700 webserver.succeed(f"systemctl start {server}-config-reload.service") 701 webserver.succeed(f"systemctl start test-renew-{server}.target") 702 check_issuer(webserver, test_domain, "pebble") 703 check_connection(client, test_domain) 704 705 with subtest("Can remove an alias from a domain + cert is updated"): 706 test_alias = f"{server}-{domains[0]}-alias.example.test" 707 switch_to(webserver, f"{server}-remove-alias") 708 webserver.wait_for_unit(f"acme-finished-{test_domain}.target") 709 wait_for_server() 710 check_connection(client, test_domain) 711 rc, _s = client.execute( 712 f"openssl s_client -CAfile /tmp/ca.crt -connect {test_alias}:443" 713 " </dev/null 2>/dev/null | openssl x509 -noout -text" 714 f" | grep DNS: | grep {test_alias}" 715 ) 716 assert rc > 0, "Removed extraDomainName was not removed from the cert" 717 718 with subtest("security.acme changes reflect on web server"): 719 # Switch back to normal server config first, reset everything. 720 switch_to(webserver, server) 721 wait_for_server() 722 switch_to(webserver, f"{server}-change-acme-conf") 723 webserver.wait_for_unit(f"acme-finished-{test_domain}.target") 724 wait_for_server() 725 check_connection_key_bits(client, test_domain, "384") 726 ''; 727}