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}