nixVersions.stable: 2.15 -> 2.17
[NixPkgs.git] / nixos / tests / acme.nix
blob4d220b9747aa67d52e9b2e7109f4a7cf35c33ce7
1 { pkgs, lib, ... }: let
2   commonConfig = ./common/acme/client;
4   dnsServerIP = nodes: nodes.dnsserver.networking.primaryIPAddress;
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   '';
18   dnsConfig = nodes: {
19     dnsProvider = "exec";
20     dnsPropagationCheck = false;
21     credentialsFile = 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   };
29   documentRoot = pkgs.runCommand "docroot" {} ''
30     mkdir -p "$out"
31     echo hello world > "$out/index.html"
32   '';
34   vhostBase = {
35     forceSSL = true;
36     locations."/".root = documentRoot;
37   };
39   vhostBaseHttpd = {
40     forceSSL = true;
41     inherit documentRoot;
42   };
44   simpleConfig = {
45     security.acme = {
46       certs."http.example.test" = {
47         listenHTTP = ":80";
48       };
49     };
51     networking.firewall.allowedTCPPorts = [ 80 ];
52   };
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   };
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         };
74         users.users."${config.services."${server}".user}".extraGroups = ["acme"];
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             };
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             };
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         };
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     };
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     };
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   };
145 in {
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   };
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     };
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     };
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 ];
183       # OpenSSL will be used for more thorough certificate validation
184       environment.systemPackages = [ pkgs.openssl ];
186       # Set log level to info so that we can see when the service is reloaded
187       services.nginx.logError = "stderr info";
189       specialisation = {
190         # Tests HTTP-01 verification using Lego's built-in web server
191         http01lego.configuration = simpleConfig;
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         ];
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         ];
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;
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         ];
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         ];
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             };
261             services.nginx.virtualHosts."slow.example.test" = {
262               forceSSL = true;
263               enableACME = true;
264               locations."/".proxyPass = "http://localhost:8000";
265             };
266           }
267         ];
269         # Test lego internal server (listenHTTP option)
270         # Also tests useRoot option
271         lego-server.configuration = { ... }: {
272           security.acme.useRoot = true;
273           security.acme.certs."lego.example.test" = {
274             listenHTTP = ":80";
275             group = "nginx";
276           };
277           services.nginx.enable = true;
278           services.nginx.virtualHosts."lego.example.test" = {
279             useACMEHost = "lego.example.test";
280             onlySSL = true;
281           };
282         };
284       # Test compatibility with Caddy
285       # It only supports useACMEHost, hence not using mkServerConfigs
286       } // (let
287         baseCaddyConfig = { nodes, config, ... }: {
288           security.acme = {
289             defaults = (dnsConfig nodes);
290             # One manual wildcard cert
291             certs."example.test" = {
292               domain = "*.example.test";
293             };
294           };
296           users.users."${config.services.caddy.user}".extraGroups = ["acme"];
298           services.caddy = {
299             enable = true;
300             virtualHosts."a.exmaple.test" = {
301               useACMEHost = "example.test";
302               extraConfig = ''
303                 root * ${documentRoot}
304               '';
305             };
306           };
307         };
308       in {
309         caddy.configuration = baseCaddyConfig;
311         # Test that the server reloads when only the acme configuration is changed.
312         "caddy-change-acme-conf".configuration = { nodes, config, ... }: lib.mkMerge [
313           (baseCaddyConfig {
314             inherit nodes config;
315           })
316           {
317             security.acme.certs."example.test" = {
318               keyType = "ec384";
319             };
320           }
321         ];
323       # Test compatibility with Nginx
324       }) // (mkServerConfigs {
325           server = "nginx";
326           group = "nginx";
327           vhostBaseData = vhostBase;
328         })
330       # Test compatibility with Apache HTTPD
331         // (mkServerConfigs {
332           server = "httpd";
333           group = "wwwrun";
334           vhostBaseData = vhostBaseHttpd;
335           extraConfig = {
336             services.httpd.adminAddr = config.security.acme.defaults.email;
337           };
338         });
339     };
341     # The client will be used to curl the webserver to validate configuration
342     client = { nodes, ... }: {
343       imports = [ commonConfig ];
344       networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
346       # OpenSSL will be used for more thorough certificate validation
347       environment.systemPackages = [ pkgs.openssl ];
348     };
349   };
351   testScript = { nodes, ... }:
352     let
353       caDomain = nodes.acme.test-support.acme.caDomain;
354       newServerSystem = nodes.webserver.config.system.build.toplevel;
355       switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test";
356     in
357     # Note, wait_for_unit does not work for oneshot services that do not have RemainAfterExit=true,
358     # this is because a oneshot goes from inactive => activating => inactive, and never
359     # reaches the active state. Targets do not have this issue.
360     ''
361       import time
364       TOTAL_RETRIES = 20
367       class BackoffTracker(object):
368           delay = 1
369           increment = 1
371           def handle_fail(self, retries, message) -> int:
372               assert retries < TOTAL_RETRIES, message
374               print(f"Retrying in {self.delay}s, {retries + 1}/{TOTAL_RETRIES}")
375               time.sleep(self.delay)
377               # Only increment after the first try
378               if retries == 0:
379                   self.delay += self.increment
380                   self.increment *= 2
382               return retries + 1
385       backoff = BackoffTracker()
388       def switch_to(node, name):
389           # On first switch, this will create a symlink to the current system so that we can
390           # quickly switch between derivations
391           root_specs = "/tmp/specialisation"
392           node.execute(
393             f"test -e {root_specs}"
394             f" || ln -s $(readlink /run/current-system)/specialisation {root_specs}"
395           )
397           switcher_path = f"/run/current-system/specialisation/{name}/bin/switch-to-configuration"
398           rc, _ = node.execute(f"test -e '{switcher_path}'")
399           if rc > 0:
400               switcher_path = f"/tmp/specialisation/{name}/bin/switch-to-configuration"
402           node.succeed(
403               f"{switcher_path} test"
404           )
407       # Ensures the issuer of our cert matches the chain
408       # and matches the issuer we expect it to be.
409       # It's a good validation to ensure the cert.pem and fullchain.pem
410       # are not still selfsigned after verification
411       def check_issuer(node, cert_name, issuer):
412           for fname in ("cert.pem", "fullchain.pem"):
413               actual_issuer = node.succeed(
414                   f"openssl x509 -noout -issuer -in /var/lib/acme/{cert_name}/{fname}"
415               ).partition("=")[2]
416               print(f"{fname} issuer: {actual_issuer}")
417               assert issuer.lower() in actual_issuer.lower()
420       # Ensure cert comes before chain in fullchain.pem
421       def check_fullchain(node, cert_name):
422           subject_data = node.succeed(
423               f"openssl crl2pkcs7 -nocrl -certfile /var/lib/acme/{cert_name}/fullchain.pem"
424               " | openssl pkcs7 -print_certs -noout"
425           )
426           for line in subject_data.lower().split("\n"):
427               if "subject" in line:
428                   print(f"First subject in fullchain.pem: {line}")
429                   assert cert_name.lower() in line
430                   return
432           assert False
435       def check_connection(node, domain, retries=0):
436           result = node.succeed(
437               "openssl s_client -brief -verify 2 -CAfile /tmp/ca.crt"
438               f" -servername {domain} -connect {domain}:443 < /dev/null 2>&1"
439           )
441           for line in result.lower().split("\n"):
442               if "verification" in line and "error" in line:
443                   retries = backoff.handle_fail(retries, f"Failed to connect to https://{domain}")
444                   return check_connection(node, domain, retries)
447       def check_connection_key_bits(node, domain, bits, retries=0):
448           result = node.succeed(
449               "openssl s_client -CAfile /tmp/ca.crt"
450               f" -servername {domain} -connect {domain}:443 < /dev/null"
451               " | openssl x509 -noout -text | grep -i Public-Key"
452           )
453           print("Key type:", result)
455           if bits not in result:
456               retries = backoff.handle_fail(retries, f"Did not find expected number of bits ({bits}) in key")
457               return check_connection_key_bits(node, domain, bits, retries)
460       def check_stapling(node, domain, retries=0):
461           # Pebble doesn't provide a full OCSP responder, so just check the URL
462           result = node.succeed(
463               "openssl s_client -CAfile /tmp/ca.crt"
464               f" -servername {domain} -connect {domain}:443 < /dev/null"
465               " | openssl x509 -noout -ocsp_uri"
466           )
467           print("OCSP Responder URL:", result)
469           if "${caDomain}:4002" not in result.lower():
470               retries = backoff.handle_fail(retries, "OCSP Stapling check failed")
471               return check_stapling(node, domain, retries)
474       def download_ca_certs(node, retries=0):
475           exit_code, _ = node.execute("curl https://${caDomain}:15000/roots/0 > /tmp/ca.crt")
476           exit_code_2, _ = node.execute(
477               "curl https://${caDomain}:15000/intermediate-keys/0 >> /tmp/ca.crt"
478           )
480           if exit_code + exit_code_2 > 0:
481               retries = backoff.handle_fail(retries, "Failed to connect to pebble to download root CA certs")
482               return download_ca_certs(node, retries)
485       start_all()
487       dnsserver.wait_for_unit("pebble-challtestsrv.service")
488       client.wait_for_unit("default.target")
490       client.succeed(
491           'curl --data \'{"host": "${caDomain}", "addresses": ["${nodes.acme.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a'
492       )
494       acme.wait_for_unit("network-online.target")
495       acme.wait_for_unit("pebble.service")
497       download_ca_certs(client)
499       # Perform http-01 w/ lego test first
500       with subtest("Can request certificate with Lego's built in web server"):
501           switch_to(webserver, "http01lego")
502           webserver.wait_for_unit("acme-finished-http.example.test.target")
503           check_fullchain(webserver, "http.example.test")
504           check_issuer(webserver, "http.example.test", "pebble")
506       # Perform renewal test
507       with subtest("Can renew certificates when they expire"):
508           hash = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem")
509           switch_to(webserver, "renew")
510           webserver.wait_for_unit("acme-finished-http.example.test.target")
511           check_fullchain(webserver, "http.example.test")
512           check_issuer(webserver, "http.example.test", "pebble")
513           hash_after = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem")
514           assert hash != hash_after
516       # Perform account change test
517       with subtest("Handles email change correctly"):
518           hash = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem")
519           switch_to(webserver, "accountchange")
520           webserver.wait_for_unit("acme-finished-http.example.test.target")
521           check_fullchain(webserver, "http.example.test")
522           check_issuer(webserver, "http.example.test", "pebble")
523           hash_after = webserver.succeed("sha256sum /var/lib/acme/http.example.test/cert.pem")
524           # Has to do a full run to register account, which creates new certs.
525           assert hash != hash_after
527       # Perform general tests
528       switch_to(webserver, "general")
530       with subtest("Can request certificate with HTTP-01 challenge"):
531           webserver.wait_for_unit("acme-finished-a.example.test.target")
532           check_fullchain(webserver, "a.example.test")
533           check_issuer(webserver, "a.example.test", "pebble")
534           webserver.wait_for_unit("nginx.service")
535           check_connection(client, "a.example.test")
537       with subtest("Runs 1 cert for account creation before others"):
538           webserver.wait_for_unit("acme-finished-b.example.test.target")
539           webserver.wait_for_unit("acme-finished-c.example.test.target")
540           check_connection(client, "b.example.test")
541           check_connection(client, "c.example.test")
543       with subtest("Certificates and accounts have safe + valid permissions"):
544           # Nginx will set the group appropriately when enableACME is used
545           group = "nginx"
546           webserver.succeed(
547               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"
548           )
549           webserver.succeed(
550               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"
551           )
552           webserver.succeed(
553               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"
554           )
555           webserver.succeed(
556               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"
557           )
559       # Selfsigned certs tests happen late so we aren't fighting the system init triggering cert renewal
560       with subtest("Can generate valid selfsigned certs"):
561           webserver.succeed("systemctl clean acme-a.example.test.service --what=state")
562           webserver.succeed("systemctl start acme-selfsigned-a.example.test.service")
563           check_fullchain(webserver, "a.example.test")
564           check_issuer(webserver, "a.example.test", "minica")
565           # Check selfsigned permissions
566           webserver.succeed(
567               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"
568           )
569           # Will succeed if nginx can load the certs
570           webserver.succeed("systemctl start nginx-config-reload.service")
572       with subtest("Correctly implements OCSP stapling"):
573           switch_to(webserver, "ocsp-stapling")
574           webserver.wait_for_unit("acme-finished-a.example.test.target")
575           check_stapling(client, "a.example.test")
577       with subtest("Can request certificate with HTTP-01 using lego's internal web server"):
578           switch_to(webserver, "lego-server")
579           webserver.wait_for_unit("acme-finished-lego.example.test.target")
580           webserver.wait_for_unit("nginx.service")
581           webserver.succeed("echo HENLO && systemctl cat nginx.service")
582           webserver.succeed("test \"$(stat -c '%U' /var/lib/acme/* | uniq)\" = \"root\"")
583           check_connection(client, "a.example.test")
584           check_connection(client, "lego.example.test")
586       with subtest("Can request certificate with HTTP-01 when nginx startup is delayed"):
587           webserver.execute("systemctl stop nginx")
588           switch_to(webserver, "slow-startup")
589           webserver.wait_for_unit("acme-finished-slow.example.test.target")
590           check_issuer(webserver, "slow.example.test", "pebble")
591           webserver.wait_for_unit("nginx.service")
592           check_connection(client, "slow.example.test")
594       with subtest("Works with caddy"):
595           switch_to(webserver, "caddy")
596           webserver.wait_for_unit("acme-finished-example.test.target")
597           webserver.wait_for_unit("caddy.service")
598           # FIXME reloading caddy is not sufficient to load new certs.
599           # Restart it manually until this is fixed.
600           webserver.succeed("systemctl restart caddy.service")
601           check_connection(client, "a.example.test")
603       with subtest("security.acme changes reflect on caddy"):
604           switch_to(webserver, "caddy-change-acme-conf")
605           webserver.wait_for_unit("acme-finished-example.test.target")
606           webserver.wait_for_unit("caddy.service")
607           # FIXME reloading caddy is not sufficient to load new certs.
608           # Restart it manually until this is fixed.
609           webserver.succeed("systemctl restart caddy.service")
610           check_connection_key_bits(client, "a.example.test", "384")
612       domains = ["http", "dns", "wildcard"]
613       for server, logsrc in [
614           ("nginx", "journalctl -n 30 -u nginx.service"),
615           ("httpd", "tail -n 30 /var/log/httpd/*.log"),
616       ]:
617           wait_for_server = lambda: webserver.wait_for_unit(f"{server}.service")
618           with subtest(f"Works with {server}"):
619               try:
620                   switch_to(webserver, server)
621                   # Skip wildcard domain for this check ([:-1])
622                   for domain in domains[:-1]:
623                       webserver.wait_for_unit(
624                           f"acme-finished-{server}-{domain}.example.test.target"
625                       )
626               except Exception as err:
627                   _, output = webserver.execute(
628                       f"{logsrc} && ls -al /var/lib/acme/acme-challenge"
629                   )
630                   print(output)
631                   raise err
633               wait_for_server()
635               for domain in domains[:-1]:
636                   check_issuer(webserver, f"{server}-{domain}.example.test", "pebble")
637               for domain in domains:
638                   check_connection(client, f"{server}-{domain}.example.test")
639                   check_connection(client, f"{server}-{domain}-alias.example.test")
641           test_domain = f"{server}-{domains[0]}.example.test"
643           with subtest(f"Can reload {server} when timer triggers renewal"):
644               # Switch to selfsigned first
645               webserver.succeed(f"systemctl clean acme-{test_domain}.service --what=state")
646               webserver.succeed(f"systemctl start acme-selfsigned-{test_domain}.service")
647               check_issuer(webserver, test_domain, "minica")
648               webserver.succeed(f"systemctl start {server}-config-reload.service")
649               webserver.succeed(f"systemctl start test-renew-{server}.target")
650               check_issuer(webserver, test_domain, "pebble")
651               check_connection(client, test_domain)
653           with subtest("Can remove an alias from a domain + cert is updated"):
654               test_alias = f"{server}-{domains[0]}-alias.example.test"
655               switch_to(webserver, f"{server}-remove-alias")
656               webserver.wait_for_unit(f"acme-finished-{test_domain}.target")
657               wait_for_server()
658               check_connection(client, test_domain)
659               rc, _s = client.execute(
660                   f"openssl s_client -CAfile /tmp/ca.crt -connect {test_alias}:443"
661                   " </dev/null 2>/dev/null | openssl x509 -noout -text"
662                   f" | grep DNS: | grep {test_alias}"
663               )
664               assert rc > 0, "Removed extraDomainName was not removed from the cert"
666           with subtest("security.acme changes reflect on web server"):
667               # Switch back to normal server config first, reset everything.
668               switch_to(webserver, server)
669               wait_for_server()
670               switch_to(webserver, f"{server}-change-acme-conf")
671               webserver.wait_for_unit(f"acme-finished-{test_domain}.target")
672               wait_for_server()
673               check_connection_key_bits(client, test_domain, "384")
674     '';