diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix index b3cb945e868b..2a2e816512e9 100644 --- a/nixos/modules/security/acme/default.nix +++ b/nixos/modules/security/acme/default.nix @@ -15,9 +15,21 @@ let numCerts = lib.length (builtins.attrNames cfg.certs); _24hSecs = 60 * 60 * 24; + # The placerholder email address used by lego in case none gets passed + placeholderEmail = "noemail@example.com"; + # Used to make unique paths for each cert/account config set mkHash = with builtins; val: lib.substring 0 20 (hashString "sha256" val); - mkAccountHash = acmeServer: data: mkHash "${toString acmeServer} ${data.keyType} ${data.email}"; + mkAccountHash = + acmeServer: data: + mkHash ( + lib.concatStringsSep " " [ + (toString acmeServer) + data.keyType + (if (data.email != null) then data.email else placeholderEmail) + ] + + ); accountDirRoot = "/var/lib/acme/.lego/accounts/"; isIP = @@ -273,6 +285,8 @@ let "--accept-tos" # Checking the option is covered by the assertions "--path" "." + ] + ++ lib.optionals (data.email != null) [ "--email" data.email ] @@ -580,7 +594,12 @@ let # Check if a new order is needed # We can only renew if the list of domains has not changed. # We also need an account key. Avoids #190493 - if cmp -s domainhash.txt certificates/domainhash.txt && [ -e '${certificateKey}' ] && [ -e 'certificates/${keyName}.crt' ] && [ -n "$(find accounts -name '${data.email}.key')" ]; then + if cmp -s domainhash.txt certificates/domainhash.txt && [ -e '${certificateKey}' ] && \ + [ -e 'certificates/${keyName}.crt' ] && \ + [ -n "$(find accounts -name '${ + if (data.email != null) then data.email else placeholderEmail + }.key')" ]; + then # Even if a cert is not expired, it may be revoked by the CA. # Try to renew, and silently fail if the cert is not expired. # Avoids #85794 and resolves #129838 @@ -1118,14 +1137,6 @@ in certs = lib.attrValues cfg.certs; in [ - { - assertion = cfg.defaults.email != null || lib.all (certOpts: certOpts.email != null) certs; - message = '' - You must define `security.acme.certs..email` or - `security.acme.defaults.email` to register with the CA. Note that using - many different addresses for certs may trigger account rate limits. - ''; - } { assertion = cfg.acceptTerms; message = '' diff --git a/nixos/tests/acme/http01-builtin.nix b/nixos/tests/acme/http01-builtin.nix index fce54470941f..bf37d7848779 100644 --- a/nixos/tests/acme/http01-builtin.nix +++ b/nixos/tests/acme/http01-builtin.nix @@ -52,7 +52,14 @@ in }; accountchange.configuration = { - security.acme.certs."${config.networking.fqdn}".email = "admin@example.test"; + # Providing an email address is optional + security.acme.certs."${config.networking.fqdn}".email = null; + }; + + emailplaceholder.configuration = { + # but Lego will default to this email address, which should not + # result in any change when configured + security.acme.certs."${config.networking.fqdn}".email = "noemail@example.com"; }; keytype.configuration = { @@ -157,6 +164,7 @@ in certName = nodes.builtin.networking.fqdn; caDomain = nodes.acme.test-support.acme.caDomain; in + # python '' ${(import ./utils.nix).pythonUtils} @@ -217,11 +225,23 @@ in hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem") # Has to do a full run to register account, which creates new certs. assert hash != hash_after, "Certificate was not renewed" + hash = hash_after + + builtin.succeed("systemctl stop renew-triggered.target") + switch_to(builtin, "emailplaceholder") + builtin.wait_for_unit("renew-triggered.target") + + # Check that there are still two account directories + builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2") + hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem") + assert hash == hash_after, "Implicit to explicit email placeholder renewed the certificate" + # Remove the new account directory builtin.succeed( "cd /var/lib/acme/.lego/accounts" " && ls -1 --sort=time | tee /dev/stderr | head -1 | xargs rm -rf" ) + # old_hash will be used in the preservation tests later old_hash = hash_after