Patch for CNAME support with certbot-dns-desec

Here is a patch for certbot-dns-desec 1.2.1 to fix one potential data-loss bug and add support for CNAME chaining the _acme-challenge subdomain. Please consider applying it to the project repository.

The data loss occurs when the _acme-challenge “subdomain” is a zone apex, for example if that subdomain is delegated to deSEC or if that subdomain points to a zone apex domain at deSEC via CNAME. In those cases, the plugin fails to fetch existing TXT records before it writes them back with the modifications, so in the unlikely case that the domain has TXT records, they’re lost. This is easily fixed by adding the ... to the request URL.

The bigger change is to make the plugin first lookup a CNAME on the _acme-challenge subdomain (up to a chain of 7 CNAMEs). Then the plugin modifies the target of the CNAME (chain) at deSEC, not the domain for which the certificate is requested, which could be a different domain at deSEC or may not even be hosted at deSEC. This indirection is explicitly supported by Letsencrypt, but without this patch it doesn’t work with Certbot and deSEC.

This functionality requires that the dnspython package is installed.

Here’s the patch:

--- original/dns_desec.py
+++ patched/dns_desec.py
@@ -2,6 +2,7 @@
 import json
 import logging
 import time
+import dns.resolver
 
 import requests
 from certbot import interfaces
@@ -66,6 +67,12 @@
 
     def _desec_work(self, domain, validation_name, validation, set_operator):
         client = self._get_desec_client()
+        try:
+            for _ in range(7):
+                validation_name = dns.resolver.resolve(validation_name,'CNAME')[0].target.to_text().rstrip('.')
+                logger.debug(f"CNAME lookup result: {validation_name}.")
+        except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
+            pass
         zone = client.get_authoritative_zone(validation_name)
         subname = validation_name.rsplit(zone['name'], 1)[0].rstrip('.')
         records = client.get_txt_rrset(zone, subname)
@@ -135,7 +142,7 @@
     def get_txt_rrset(self, zone, subname):
         domain = zone['name']
         response = self.desec_get(
-            url=f"{self.endpoint}/domains/{domain}/rrsets/{subname}/TXT/",
+            url=f"{self.endpoint}/domains/{domain}/rrsets/{subname}.../TXT/",
         )
 
         if response.status_code == 404:
2 Likes

Nice, thanks! Would you mind opening a PR for that at GitHub - desec-io/certbot-dns-desec: Let's Encrypt Certificates for Domains Hosted at deSEC?

Stay secure,
Peter

1 Like

I would do that, and had tried that, but Github is being a diva about opening an account without a phone number. Everything you need is in that comment.

1 Like

I see. Well, because it’s you. :slight_smile:

I made some minor modifications, but otherwise looks good. Will merge / release after 4-eyes review.

Stay secure,
Peter

2 Likes

Released just now: certbot-dns-desec · PyPI

Sorry for the delay.

Stay secure,
Peter

2 Likes

:+1: Tested with the NginxProxyManager docker image after manually updating to the latest certbot-dns-desec package version with pip. Correctly pulls dnspython dependency. After installation, authenticates with CNAME-redirected _acme-challenge subdomain. Now it’s just a matter of the update finding its way into updated docker images. Thank you!

2 Likes