nixos/acme: drop email requirement

Let's Encrypt does not require email addreses any longer, so we should
allow users not to provide any.

Unsetting the email adress will change the account hash and Lego will
start using a dummy email address instead. The address is hardcoded in
the Lego source code in the userIDPlaceholder constant.

We then verify in tests that changing between no email address and the
placeholder address does not create a new account nor rotate the previous
certificate.

This is supported since Lego 4.30.1.

https://github.com/go-acme/lego/commit/bc163db9edd23bbfc3521086c0b570f468b9a87b
This commit is contained in:
Martin Weinelt
2026-02-16 20:53:41 +01:00
parent a5021dd696
commit e65f8ef106
2 changed files with 42 additions and 11 deletions
+21 -10
View File
@@ -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/";
# Lockdir is acme-setup.service's RuntimeDirectory.
@@ -263,6 +275,8 @@ let
"--accept-tos" # Checking the option is covered by the assertions
"--path"
"."
]
++ lib.optionals (data.email != null) [
"--email"
data.email
]
@@ -544,7 +558,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
@@ -1062,14 +1081,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.<name>.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 = ''
+21 -1
View File
@@ -51,7 +51,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 = {
@@ -154,6 +161,7 @@ in
certName = nodes.builtin.networking.fqdn;
caDomain = nodes.acme.test-support.acme.caDomain;
in
# python
''
${(import ./utils.nix).pythonUtils}
@@ -212,11 +220,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