let commonConfig = ./common/letsencrypt/common.nix; in import ./make-test-python.nix { name = "acme"; nodes = rec { letsencrypt = ./common/letsencrypt; acmeStandalone = { config, pkgs, ... }: { imports = [ commonConfig ]; networking.firewall.allowedTCPPorts = [ 80 ]; networking.extraHosts = '' ${config.networking.primaryIPAddress} standalone.com ''; security.acme = { server = "https://acme-v02.api.letsencrypt.org/dir"; certs."standalone.com" = { webroot = "/var/lib/acme/acme-challenges"; }; }; systemd.targets."acme-finished-standalone.com" = {}; systemd.services."acme-standalone.com" = { wants = [ "acme-finished-standalone.com.target" ]; before = [ "acme-finished-standalone.com.target" ]; }; services.nginx.enable = true; services.nginx.virtualHosts."standalone.com" = { locations."/.well-known/acme-challenge".root = "/var/lib/acme/acme-challenges"; }; }; webserver = { config, pkgs, ... }: { imports = [ commonConfig ]; networking.firewall.allowedTCPPorts = [ 80 443 ]; networking.extraHosts = '' ${config.networking.primaryIPAddress} a.example.com ${config.networking.primaryIPAddress} b.example.com ''; # A target remains active. Use this to probe the fact that # a service fired eventhough it is not RemainAfterExit systemd.targets."acme-finished-a.example.com" = {}; systemd.services."acme-a.example.com" = { wants = [ "acme-finished-a.example.com.target" ]; before = [ "acme-finished-a.example.com.target" ]; }; services.nginx.enable = true; services.nginx.virtualHosts."a.example.com" = { enableACME = true; forceSSL = true; locations."/".root = pkgs.runCommand "docroot" {} '' mkdir -p "$out" echo hello world > "$out/index.html" ''; }; security.acme.server = "https://acme-v02.api.letsencrypt.org/dir"; nesting.clone = [ ({pkgs, ...}: { networking.extraHosts = '' ${config.networking.primaryIPAddress} b.example.com ''; systemd.targets."acme-finished-b.example.com" = {}; systemd.services."acme-b.example.com" = { wants = [ "acme-finished-b.example.com.target" ]; before = [ "acme-finished-b.example.com.target" ]; }; services.nginx.virtualHosts."b.example.com" = { enableACME = true; forceSSL = true; locations."/".root = pkgs.runCommand "docroot" {} '' mkdir -p "$out" echo hello world > "$out/index.html" ''; }; }) ]; }; client = commonConfig; }; testScript = {nodes, ...}: let newServerSystem = nodes.webserver2.config.system.build.toplevel; switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test"; in # Note, wait_for_unit does not work for oneshot services that do not have RemainAfterExit=true, # this is because a oneshot goes from inactive => activating => inactive, and never # reaches the active state. To work around this, we create some mock target units which # get pulled in by the oneshot units. The target units linger after activation, and hence we # can use them to probe that a oneshot fired. It is a bit ugly, but it is the best we can do '' client.start() letsencrypt.start() acmeStandalone.start() letsencrypt.wait_for_unit("default.target") letsencrypt.wait_for_unit("pebble.service") with subtest("can request certificate with HTTPS-01 challenge"): acmeStandalone.wait_for_unit("default.target") acmeStandalone.succeed("systemctl start acme-standalone.com.service") acmeStandalone.wait_for_unit("acme-finished-standalone.com.target") client.wait_for_unit("default.target") client.succeed("curl https://acme-v02.api.letsencrypt.org:15000/roots/0 > /tmp/ca.crt") client.succeed( "curl https://acme-v02.api.letsencrypt.org:15000/intermediate-keys/0 >> /tmp/ca.crt" ) with subtest("Can request certificate for nginx service"): webserver.wait_for_unit("acme-finished-a.example.com.target") client.succeed( "curl --cacert /tmp/ca.crt https://a.example.com/ | grep -qF 'hello world'" ) with subtest("Can add another certificate for nginx service"): webserver.succeed( "/run/current-system/fine-tune/child-1/bin/switch-to-configuration test" ) webserver.wait_for_unit("acme-finished-b.example.com.target") client.succeed( "curl --cacert /tmp/ca.crt https://b.example.com/ | grep -qF 'hello world'" ) ''; }