at master 12 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 domain = "example.test"; 9in 10{ 11 name = "http01-builtin"; 12 meta = { 13 maintainers = lib.teams.acme.members; 14 # Hard timeout in seconds. Average run time is about 90 seconds. 15 timeout = 300; 16 }; 17 18 nodes = { 19 # The fake ACME server which will respond to client requests 20 acme = 21 { nodes, ... }: 22 { 23 imports = [ ../common/acme/server ]; 24 }; 25 26 builtin = 27 { nodes, config, ... }: 28 { 29 imports = [ ../common/acme/client ]; 30 networking.domain = domain; 31 networking.firewall.allowedTCPPorts = [ 80 ]; 32 33 # OpenSSL will be used for more thorough certificate validation 34 environment.systemPackages = [ pkgs.openssl ]; 35 36 security.acme.certs."${config.networking.fqdn}" = { 37 listenHTTP = ":80"; 38 }; 39 40 systemd.targets."renew-triggered" = { 41 wantedBy = [ "acme-order-renew-${config.networking.fqdn}.service" ]; 42 after = [ "acme-order-renew-${config.networking.fqdn}.service" ]; 43 unitConfig.RefuseManualStart = true; 44 }; 45 46 specialisation = { 47 renew.configuration = { 48 # Pebble provides 5 year long certs, 49 # needs to be higher than that to test renewal 50 security.acme.certs."${config.networking.fqdn}".validMinDays = 9999; 51 }; 52 53 accountchange.configuration = { 54 security.acme.certs."${config.networking.fqdn}".email = "admin@example.test"; 55 }; 56 57 keytype.configuration = { 58 security.acme.certs."${config.networking.fqdn}".keyType = "ec384"; 59 }; 60 61 # Perform http-01 test again, but using the pre-24.05 account hashing 62 # (see https://github.com/NixOS/nixpkgs/pull/317257) 63 # The hash is deterministic in this case - only based on keyType and email. 64 # Note: This test is making the assumption that the acme module will create 65 # the account directory regardless of internet connectivity or server reachability. 66 legacy_account_hash.configuration = { 67 security.acme.defaults.server = lib.mkForce null; 68 }; 69 70 ocsp_stapling.configuration = { 71 security.acme.certs."${config.networking.fqdn}".ocspMustStaple = true; 72 }; 73 74 preservation.configuration = { }; 75 76 add_cert_and_domain.configuration = { 77 security.acme.certs = { 78 "${config.networking.fqdn}" = { 79 extraDomainNames = [ 80 "builtin-alt.${domain}" 81 ]; 82 }; 83 # We can assume that if renewal succeeds then the account creation leader 84 # logic is working, since only one service could bind to port 80 at the same time. 85 "builtin-2.${domain}".listenHTTP = ":80"; 86 }; 87 # To make sure it's the account creation leader that is doing the work. 88 security.acme.maxConcurrentRenewals = 10; 89 }; 90 91 concurrency.configuration = { 92 # As above, relying on port binding behaviour to assert that concurrency limit 93 # prevents > 1 service running at a time. 94 security.acme.maxConcurrentRenewals = 1; 95 security.acme.certs = { 96 "${config.networking.fqdn}" = { 97 extraDomainNames = [ 98 "builtin-alt.${domain}" 99 ]; 100 }; 101 "builtin-2.${domain}" = { 102 extraDomainNames = [ "builtin-2-alt.${domain}" ]; 103 listenHTTP = ":80"; 104 }; 105 "builtin-3.${domain}".listenHTTP = ":80"; 106 }; 107 }; 108 109 csr.configuration = 110 let 111 conf = pkgs.writeText "openssl.csr.conf" '' 112 [req] 113 default_bits = 2048 114 prompt = no 115 default_md = sha256 116 req_extensions = req_ext 117 distinguished_name = dn 118 119 [ dn ] 120 CN = ${config.networking.fqdn} 121 122 [ req_ext ] 123 subjectAltName = @alt_names 124 125 [ alt_names ] 126 DNS.1 = ${config.networking.fqdn} 127 ''; 128 csrData = 129 pkgs.runCommandNoCC "csr-and-key" 130 { 131 buildInputs = [ pkgs.openssl ]; 132 } 133 '' 134 mkdir -p $out 135 openssl req -new -newkey rsa:2048 -nodes \ 136 -keyout $out/key.pem \ 137 -out $out/request.csr \ 138 -config ${conf} 139 ''; 140 in 141 { 142 security.acme.certs."${config.networking.fqdn}" = { 143 csr = "${csrData}/request.csr"; 144 csrKey = "${csrData}/key.pem"; 145 }; 146 }; 147 }; 148 }; 149 }; 150 151 testScript = 152 { nodes, ... }: 153 let 154 certName = nodes.builtin.networking.fqdn; 155 caDomain = nodes.acme.test-support.acme.caDomain; 156 in 157 '' 158 ${(import ./utils.nix).pythonUtils} 159 160 domain = "${domain}" 161 cert = "${certName}" 162 cert2 = "builtin-2." + domain 163 cert3 = "builtin-3." + domain 164 legacy_account_dir = "/var/lib/acme/.lego/accounts/1ccf607d9aa280e9af00" 165 166 acme.start() 167 wait_for_running(acme) 168 acme.wait_for_open_port(443) 169 170 with subtest("Boot and acquire a new cert"): 171 builtin.start() 172 wait_for_running(builtin) 173 174 check_issuer(builtin, cert, "pebble") 175 check_domain(builtin, cert, cert) 176 177 with subtest("Validate permissions"): 178 check_permissions(builtin, cert, "acme") 179 180 with subtest("Check renewal behaviour"): 181 # First, test no-op behaviour 182 hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem") 183 # old_hash will be used in the preservation tests later 184 old_hash = hash 185 builtin.succeed(f"systemctl start acme-{cert}.service") 186 builtin.succeed(f"systemctl start acme-order-renew-{cert}.service") 187 builtin.wait_for_unit("renew-triggered.target") 188 189 hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem") 190 assert hash == hash_after, "Certificate was unexpectedly changed" 191 192 builtin.succeed("systemctl stop renew-triggered.target") 193 switch_to(builtin, "renew") 194 builtin.wait_for_unit("renew-triggered.target") 195 196 check_issuer(builtin, cert, "pebble") 197 hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem | tee /dev/stderr") 198 assert hash != hash_after, "Certificate was not renewed" 199 200 check_permissions(builtin, cert, "acme") 201 202 with subtest("Handles email change correctly"): 203 hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem") 204 205 builtin.succeed("systemctl stop renew-triggered.target") 206 switch_to(builtin, "accountchange") 207 builtin.wait_for_unit("renew-triggered.target") 208 209 check_issuer(builtin, cert, "pebble") 210 # Check that there are now 2 account directories 211 builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2") 212 hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem") 213 # Has to do a full run to register account, which creates new certs. 214 assert hash != hash_after, "Certificate was not renewed" 215 # Remove the new account directory 216 builtin.succeed( 217 "cd /var/lib/acme/.lego/accounts" 218 " && ls -1 --sort=time | tee /dev/stderr | head -1 | xargs rm -rf" 219 ) 220 # old_hash will be used in the preservation tests later 221 old_hash = hash_after 222 223 check_permissions(builtin, cert, "acme") 224 225 with subtest("Correctly implements OCSP stapling"): 226 check_stapling(builtin, cert, "${caDomain}", fail=True) 227 228 builtin.succeed("systemctl stop renew-triggered.target") 229 switch_to(builtin, "ocsp_stapling") 230 builtin.wait_for_unit("renew-triggered.target") 231 232 check_stapling(builtin, cert, "${caDomain}") 233 check_permissions(builtin, cert, "acme") 234 235 with subtest("Handles keyType change correctly"): 236 check_key_bits(builtin, cert, 256) 237 238 builtin.succeed("systemctl stop renew-triggered.target") 239 switch_to(builtin, "keytype") 240 builtin.wait_for_unit("renew-triggered.target") 241 242 check_key_bits(builtin, cert, 384) 243 # keyType is part of the accountHash, thus a new account will be created 244 builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2") 245 check_permissions(builtin, cert, "acme") 246 247 with subtest("Reuses generated, valid certs from previous configurations"): 248 # Right now, the hash should not match due to the previous test 249 hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem | tee /dev/stderr") 250 assert hash != old_hash, "Expected certificate to differ" 251 252 builtin.succeed("systemctl stop renew-triggered.target") 253 switch_to(builtin, "preservation") 254 builtin.wait_for_unit("renew-triggered.target") 255 256 hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem | tee /dev/stderr") 257 assert hash == old_hash, "Expected certificate to match from older configuration" 258 check_permissions(builtin, cert, "acme") 259 260 with subtest("Add a new cert, extend existing cert domains"): 261 check_domain(builtin, cert, f"builtin-alt.{domain}", fail=True) 262 263 builtin.succeed("systemctl stop renew-triggered.target") 264 switch_to(builtin, "add_cert_and_domain") 265 builtin.wait_for_unit("renew-triggered.target") 266 267 check_issuer(builtin, cert, "pebble") 268 check_domain(builtin, cert, f"builtin-alt.{domain}") 269 check_issuer(builtin, cert2, "pebble") 270 check_domain(builtin, cert2, cert2) 271 # There should not be a new account folder created 272 builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2") 273 check_permissions(builtin, cert, "acme") 274 check_permissions(builtin, cert2, "acme") 275 276 with subtest("Check account hashing compatibility with pre-24.05 settings"): 277 builtin.succeed("systemctl stop renew-triggered.target") 278 switch_to(builtin, "legacy_account_hash" 279 ) 280 builtin.wait_for_unit("renew-triggered.target") 281 282 builtin.succeed(f"stat {legacy_account_dir} > /dev/stderr && rm -rf {legacy_account_dir}") 283 check_permissions(builtin, cert, "acme") 284 285 with subtest("Ensure concurrency limits work"): 286 builtin.succeed("systemctl stop renew-triggered.target") 287 switch_to(builtin, "concurrency") 288 builtin.wait_for_unit("renew-triggered.target") 289 290 check_issuer(builtin, cert3, "pebble") 291 check_domain(builtin, cert3, cert3) 292 check_permissions(builtin, cert, "acme") 293 294 with subtest("Can renew using a CSR"): 295 builtin.succeed(f"systemctl stop acme-{cert}.service") 296 builtin.succeed(f"systemctl clean acme-{cert}.service --what=state") 297 298 builtin.succeed("systemctl stop renew-triggered.target") 299 switch_to(builtin, "csr") 300 builtin.wait_for_unit("renew-triggered.target") 301 302 check_issuer(builtin, cert, "pebble") 303 304 with subtest("Generate self-signed certs"): 305 acme.shutdown() 306 307 check_issuer(builtin, cert, "pebble") 308 309 builtin.succeed(f"systemctl stop acme-{cert}.service") 310 builtin.succeed(f"systemctl clean acme-{cert}.service --what=state") 311 builtin.succeed(f"systemctl start acme-{cert}.service") 312 313 check_issuer(builtin, cert, "minica") 314 check_domain(builtin, cert, cert) 315 316 with subtest("Validate permissions (self-signed)"): 317 check_permissions(builtin, cert, "acme") 318 319 ''; 320}