stuvusIT / danebot

Automated renewal of TLSA records for renewed certificates
MIT License
0 stars 1 forks source link

How to create TLSA RR with rollover private key? #10

Open blackbit42 opened 2 months ago

blackbit42 commented 2 months ago

To avoid issues with excessively caching DNS resolvers (i.e. resolvers that do not honor TTL), it's common practice to generate a so called rollover private key when a TLS certificate is refreshed, e.g. via ACME. How can generate (and deploy) a TLSA RR for a rollover key with danebot? A naive attempt - pointing danebot to the last certificate and the new key - unsurprisingly results in:

Error: key does not belong to certificate: public key mismatch

as it's explicitly checked if the separately supplied private key matches the certificate.

Is support for rollover keys unimplemented? If so, can this be added?

blackbit42 commented 2 months ago

I took the liberty to extract the relevant portions of danebot into a minimal proof-of-concept tool that derives a TLSA RR from a provided private key alone. When I run it against the private key of my certificate, I get the same TLSA RR as with danebot. When I run it against the rollover private key that I've generated with dehydrated, I get a TLSA RR too.

privkey2tlsa.py:

import sys

import dns.rdtypes.ANY.TLSA

from cryptography.hazmat.primitives import hashes, serialization

with open(sys.argv[1], "rb") as priv_key_fp:
    priv_key = priv_key_fp.read()

priv_key_pem = serialization.load_pem_private_key(priv_key, password=None)
pub_key = priv_key_pem.public_key()
pub_key_der = pub_key.public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo)

digest = hashes.Hash(hashes.SHA256())
digest.update(pub_key_der)
dane_ee_hash = digest.finalize()
rdata = dns.rdtypes.ANY.TLSA.TLSA(dns.rdataclass.IN, dns.rdatatype.TLSA, 3, 1, 1, dane_ee_hash)

print("rdata:", rdata)
haslersn commented 2 months ago

Danebot supports keeping both the old and new TLSA record for a certain amount of time (namely propagation time plus TTL). This should be sufficient for your use case. In order to account for excessively caching DNS resolvers, simply set --propagation-time to something higher than the default of 60 (seconds).

blackbit42 commented 2 months ago

The idea is not to keep the current and one or more old entries, but have a TLSA RR for the current key and a future key. Typically, for the future key there is no certificate yet, as you don't want to have time time ticking on the expiration date without the certificate being used. This is done to allow DNS resolvers to pick up the TLSA RR for the 'next' key ahead of time, so that once the certificate is swapped out, there is no uncertainty about old entries in DNS caches.

haslersn commented 2 months ago

This will not be supported by danebot, because I use danebot together with lego which has (as far as I know) no way of providing a pre-created key to the renewal processs.

I also don't accept PRs for features which are broadly out of scope for my use case. I'm sorry for that.

Regarding the time ticking: When using Let's Encrypt, you have one month for renewing the certificate, creating the new TLSA record and waiting for it to be propagated. This is plenty of time and should be sufficient.

haslersn commented 2 months ago

so that once the certificate is swapped out, there is no uncertainty about old entries in DNS caches.

Not sure what you mean by that. danebot deletes the old record as soon as the certificate is swapped out. Of course there could still be old entries in DNS caches until they refresh, but that's nothing we can prevent. The important thing is that the DNS caches also have the new entry, because we waited long enough before swapping out the certificate. (Swapping out the certificate is supposed to be done by the hook provided to danebot via --hook.)

blackbit42 commented 2 months ago

This will not be supported by danebot

I also don't accept PRs for features which are broadly out of scope for my use case.

Understood.

When using Let's Encrypt, you have one month for renewing the certificate, creating the new TLSA record and waiting for it to be propagated. This is plenty of time and should be sufficient.

My aim is to maximize the time (spec-violating) DNS resolvers have to pick up records. Is another approach 'good enough'? Probably, but I don't see an advantage in letting time pass.

Of course there could still be old entries in DNS caches until they refresh, but that's nothing we can prevent.

True, we cannot prevent that, but that's not an issue anyways. Old entries do no harm.

The important thing is that the DNS caches also have the new entry

Correct. Though, 'new' in this context is the 'next' or 'future' entry according to my train of thought, not the current one.

Please contemplate the following scenario without a 'next' TLSA RR.

  1. You renew your certificate
  2. A DANE-aware client connects
  3. The DNS resolver is caching excessively and has a TLSA RR for this domain in the cache and therefore won't query again
  4. the TLSA RR will mismatch the certificate -> connection failure
haslersn commented 2 months ago

It seems that our approaches differ only in the time when the new certificate is created. (By "creating", I mean the whole certificate renewal process including ACME.) Your approach would be to create the certificate just before installing it on the server, i.e., after propagation of the new TLSA record. My approach (i.e., the currently implemented one) is to create the certificate immediately after creating the new key, i.e., before propagation of the new TLSA record.

In both cases there will be no connection failures, if we wait long enough for propagation.

Of course, your approach has the advantage that the validity period (i.e., time from creation to expiration) starts ticking at a later point.

However, your approach requires creating the key beforehand, i.e., temporally separated from the certificate renewal process. This somehow has to be automated, because we don't want to do anything manually. Are there any certificate management tools (such as certbot or lego) that support automatically creating the key for the next renewal at an earlier point in time, e.g., configurable x days before certificate renewal?

blackbit42 commented 2 months ago

It seems that our approaches differ only in the time when the new certificate is created. [...]

Agreed.

In both cases there will be no connection failures, if we wait long enough for propagation.

Agreed.

Of course, your approach has the advantage that the validity period (i.e., time from creation to expiration) starts ticking at a later point.

Agreed.

However, your approach requires creating the key beforehand, i.e., temporally separated from the certificate renewal process.

Correct.

Are there any certificate management tools (such as certbot or lego) that support automatically creating the key for the next renewal at an earlier point in time, e.g., configurable x days before certificate renewal?

I will admit that I haven't studied features of other ACME clients besides the one I currently use, which is dehydrated. dehydrated has a configuration setting called PRIVATE_KEY_ROLLOVER.

# Create an extra private key for rollover (default: no)
#PRIVATE_KEY_ROLLOVER="no"

It does precisely what I want and what several recommendations for DANE on the web say, e.g. this.

After a run of dehydrated that renewed the certificate of a given domain, you end up with two key files:

I somewhat assumed that is functionality that all ACME clients provide, but I can't say I have actually checked. Unless you want to do DANE, there is no point in having that function I think.