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