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: