at master 8.8 kB view raw
1{ 2 serverName, 3 group, 4 baseModule, 5 domain, 6}: 7{ 8 config, 9 lib, 10 pkgs, 11 ... 12}: 13{ 14 name = serverName; 15 meta = { 16 maintainers = lib.teams.acme.members; 17 # Hard timeout in seconds. Average run time is about 100 seconds. 18 timeout = 300; 19 }; 20 21 interactive.sshBackdoor.enable = true; 22 23 nodes = { 24 # The fake ACME server which will respond to client requests 25 acme = 26 { nodes, ... }: 27 { 28 imports = [ ../common/acme/server ]; 29 }; 30 31 webserver = 32 { nodes, ... }: 33 { 34 imports = [ 35 ../common/acme/client 36 baseModule 37 ]; 38 networking.domain = domain; 39 networking.firewall.allowedTCPPorts = [ 40 80 41 443 42 ]; 43 44 # Resolve the vhosts the easy way 45 networking.hosts."127.0.0.1" = [ 46 "proxied.${domain}" 47 "certchange.${domain}" 48 "zeroconf.${domain}" 49 "zeroconf2.${domain}" 50 "zeroconf3.${domain}" 51 "nullroot.${domain}" 52 ]; 53 54 # OpenSSL will be used for more thorough certificate validation 55 environment.systemPackages = [ pkgs.openssl ]; 56 57 # Used to determine if service reload was triggered. 58 # This does not provide a guarantee that the webserver is finished reloading, 59 # to handle that there is retry logic wrapping any connectivity checks. 60 systemd.targets."renew-triggered" = { 61 wantedBy = [ "${serverName}-config-reload.service" ]; 62 after = [ "${serverName}-config-reload.service" ]; 63 unitConfig.RefuseManualStart = true; 64 }; 65 66 security.acme.certs."proxied.${domain}" = { 67 listenHTTP = ":8080"; 68 group = group; 69 }; 70 71 specialisation = { 72 # Test that the web server is correctly reloaded when the cert changes 73 certchange.configuration = { 74 security.acme.certs."proxied.${domain}".extraDomainNames = [ 75 "certchange.${domain}" 76 ]; 77 }; 78 79 # A useful transitional step before other tests, and tests behaviour 80 # of removing an extra domain from a cert. 81 certundo.configuration = { }; 82 83 # Tests these features: 84 # - enableACME behaves as expected 85 # - serverAliases are appended to extraDomainNames 86 # - Correct routing to the specific virtualHost for a cert 87 # Inherits previous test config 88 zeroconf.configuration = { 89 services.${serverName}.virtualHosts."zeroconf.${domain}" = { 90 addSSL = true; 91 enableACME = true; 92 serverAliases = [ "zeroconf2.${domain}" ]; 93 }; 94 }; 95 96 # Test that serverAliases are correctly removed which triggers 97 # cert regeneration and service reload. 98 rmalias.configuration = { 99 services.${serverName}.virtualHosts."zeroconf.${domain}" = { 100 addSSL = true; 101 enableACME = true; 102 }; 103 }; 104 105 # Test that "acmeRoot = null" still results in 106 # valid cert generation by inheriting defaults. 107 nullroot.configuration = { 108 # The default.nix has the server-type dependent config statements 109 # to properly set up the proxying. We need a separate port here to 110 # avoid hostname issues with the proxy already running on :8080 111 security.acme.defaults.listenHTTP = ":8081"; 112 services.${serverName}.virtualHosts."nullroot.${domain}" = { 113 addSSL = true; 114 enableACME = true; 115 acmeRoot = null; 116 }; 117 }; 118 119 # Test that a adding a second virtual host will not trigger 120 # other units (account and renewal service for first) 121 zeroconf3.configuration = { 122 services.${serverName}.virtualHosts = { 123 "zeroconf.${domain}" = { 124 addSSL = true; 125 enableACME = true; 126 serverAliases = [ "zeroconf2.${domain}" ]; 127 }; 128 "zeroconf3.${domain}" = { 129 addSSL = true; 130 enableACME = true; 131 }; 132 }; 133 # We're doing something risky with the combination of the service unit being persistent 134 # that could end up that the timers do not trigger properly. Show that timers have the 135 # desired effect. 136 systemd.timers."acme-renew-zeroconf3.${domain}".timerConfig = { 137 OnCalendar = lib.mkForce "*-*-* *:*:0/5"; 138 AccuracySec = lib.mkForce 0; 139 # Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/. 140 RandomizedDelaySec = lib.mkForce 0; 141 FixedRandomDelay = lib.mkForce 0; 142 }; 143 }; 144 }; 145 }; 146 }; 147 148 testScript = 149 { nodes, ... }: 150 '' 151 ${(import ./utils.nix).pythonUtils} 152 153 domain = "${domain}" 154 ca_domain = "${nodes.acme.test-support.acme.caDomain}" 155 fqdn = f"proxied.{domain}" 156 157 webserver.start() 158 webserver.wait_for_unit("${serverName}.service") 159 160 with subtest("Can run on self-signed certificates"): 161 check_issuer(webserver, fqdn, "minica") 162 # Check that the web server has picked up the selfsigned cert 163 check_connection(webserver, fqdn, minica=True) 164 165 acme.start() 166 wait_for_running(acme) 167 acme.wait_for_open_port(443) 168 169 with subtest("Acquire a cert through a proxied lego"): 170 webserver.succeed(f"systemctl start acme-order-renew-{fqdn}.service") 171 webserver.wait_for_unit("renew-triggered.target") 172 download_ca_certs(webserver, ca_domain) 173 check_issuer(webserver, fqdn, "pebble") 174 check_connection(webserver, fqdn) 175 176 with subtest("security.acme changes reflect on web server part 1"): 177 check_connection(webserver, f"certchange.{domain}", fail=True) 178 switch_to(webserver, "certchange") 179 webserver.wait_for_unit("renew-triggered.target") 180 check_connection(webserver, f"certchange.{domain}") 181 check_connection(webserver, fqdn) 182 183 with subtest("security.acme changes reflect on web server part 2"): 184 check_connection(webserver, f"certchange.{domain}") 185 switch_to(webserver, "certundo") 186 webserver.wait_for_unit("renew-triggered.target") 187 check_connection(webserver, f"certchange.{domain}", fail=True) 188 check_connection(webserver, fqdn) 189 190 with subtest("Zero configuration SSL certificates for a vhost"): 191 check_connection(webserver, f"zeroconf.{domain}", fail=True) 192 switch_to(webserver, "zeroconf") 193 webserver.wait_for_unit("renew-triggered.target") 194 check_connection(webserver, f"zeroconf.{domain}") 195 check_connection(webserver, f"zeroconf2.{domain}") 196 check_connection(webserver, fqdn) 197 198 with subtest("Removing an alias from a vhost"): 199 check_connection(webserver, f"zeroconf2.{domain}") 200 switch_to(webserver, "rmalias") 201 webserver.wait_for_unit("renew-triggered.target") 202 check_connection(webserver, f"zeroconf2.{domain}", fail=True) 203 check_connection(webserver, f"zeroconf.{domain}") 204 check_connection(webserver, fqdn) 205 206 with subtest("Create cert using inherited default validation mechanism"): 207 check_connection(webserver, f"nullroot.{domain}", fail=True) 208 switch_to(webserver, "nullroot") 209 webserver.wait_for_unit("renew-triggered.target") 210 check_connection(webserver, f"nullroot.{domain}") 211 212 with subtest("Ensure that adding a second vhost does not trigger first vhost acme units"): 213 switch_to(webserver, "zeroconf") 214 webserver.wait_for_unit("renew-triggered.target") 215 webserver.succeed("journalctl --cursor-file=/tmp/cursor | grep acme") 216 switch_to(webserver, "zeroconf3") 217 webserver.wait_for_unit("renew-triggered.target") 218 output = webserver.succeed("journalctl --cursor-file=/tmp/cursor | grep acme") 219 # The new certificate unit gets triggered: 220 t.assertIn(f"acme-zeroconf3.{domain}-start", output) 221 # The account generation should not be triggered again: 222 t.assertNotIn("acme-account-d590213ed52603e9128d.target", output) 223 # The other certificates should also not be triggered: 224 t.assertNotIn(f"acme-zeroconf.{domain}-start", output) 225 t.assertNotIn(f"acme-proxied.{domain}-start", output) 226 # Ensure the timer works, due to our shenanigans with 227 # RemainAfterExit=true 228 webserver.wait_until_succeeds(f"journalctl --cursor-file=/tmp/cursor | grep 'Starting Order (and renew) ACME certificate for zeroconf3.{domain}...'") 229 ''; 230}