Repository: certbot/certbot

Issue:
rfc2136 broken with hidden primary setups
https://github.com/certbot/certbot/pull/9408 has caused certbot to fail in hidden master setups because the SOA query fails due to an unknown TSIG key.

For security reasons, keys are only stored on a hidden primary, hence the secondary rejects the TSIG signed dns query.

suggest this commit be reverted or logic added to make TSIG signatures on DNS queries optional.
Fundamentally, TSIG is only required on DDNS updates (and zone transfers)


Discussion:
What if we put a retry on the SOA query, without authentication?
I think retrying the query without TSIG is a not a good solution because:

1. Spurious NotAuth traffic and log entries are a hindrance to administration and troubleshooting;
2. It is entirely unclear to me what purpose this change serves given that BIND has 'in-view' in all supported versions and documentation on how to use it can be found in [Special considerations for multiple views in BIND](https://certbot-dns-rfc2136.readthedocs.io/en/stable/#special-considerations-for-multiple-views-in-bind);
3. If indeed signed queries are a genuine requirement for some, it would only be needed rarely and hence this functionality should be optional and disabled by default; and
4. You seem to already have a better thought out implementation in https://github.com/certbot/certbot/pull/9106 

OK, thanks for the feedback.
I would suggest that https://github.com/certbot/certbot/pull/9408 be simply reverted. It is flawed and unnecessary for the reasons given above.

Resolving patch:
diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py
--- a/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py
+++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py
@@ -26,8 +26,8 @@
 
 Use of this plugin requires a configuration file containing the target DNS
 server and optional port that supports RFC 2136 Dynamic Updates, the name
-of the TSIG key, the TSIG key secret itself and the algorithm used if it's
-different to HMAC-MD5.
+of the TSIG key, the TSIG key secret itself, the algorithm used if it's
+different to HMAC-MD5, and optionally whether to sign the initial SOA query.
 
 .. code-block:: ini
    :name: credentials.ini
@@ -44,6 +44,9 @@
 AmKd7ak51vWKgSl12ib86oQRPkpDjg==
    # TSIG key algorithm
    dns_rfc2136_algorithm = HMAC-SHA512
+   # TSIG sign SOA query (optional, default: false)
+   dns_rfc2136_sign_query = false
+
 
 The path to this file can be provided interactively or using the
 ``--dns-rfc2136-credentials`` command-line argument. Certbot records the
@@ -194,4 +197,34 @@
      };
    };
 
+Another solution is to add `dns_rfc2136_sign_query = true` to the configuration file and then
+add the key to the `match-clients` list within the external zone view. All queries signed with
+this key should then be directed to this view, regardless of source IP.
+
+.. code-block:: none
+   :caption: Split-view BIND configuration with key-based ACLs
+
+   key "keyname." {
+     algorithm hmac-sha512;
+     secret "4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \
+AmKd7ak51vWKgSl12ib86oQRPkpDjg==";
+   };
+
+   // adjust internal-addresses to suit your needs
+   acl internal-address { 127.0.0.0/8; 10.0.0.0/8; 192.168.0.0/16; 172.16.0.0/12; };
+
+   acl certbot-keys { key keyname.; }
+
+   view "external" {
+     match-clients { acl certbot-keys; !internal-addresses; any; };
+
+     zone "example.com." IN {
+       type master;
+       file "named.example.com";
+       update-policy {
+         grant keyname. name _acme-challenge.example.com. txt;
+       };
+     };
+   };
+
 """
diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
--- a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
+++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
@@ -90,12 +90,14 @@ def _cleanup(self, _domain: str, validation_name: str, validation: str) -> None:
     def _get_rfc2136_client(self) -> "_RFC2136Client":
         if not self.credentials:  # pragma: no cover
             raise errors.Error("Plugin has not been prepared.")
+
         return _RFC2136Client(cast(str, self.credentials.conf('server')),
                               int(cast(str, self.credentials.conf('port')) or self.PORT),
                               cast(str, self.credentials.conf('name')),
                               cast(str, self.credentials.conf('secret')),
                               self.ALGORITHMS.get(self.credentials.conf('algorithm') or '',
-                                                  dns.tsig.HMAC_MD5))
+                                                  dns.tsig.HMAC_MD5),
+                              (self.credentials.conf('sign_query') or '').upper() == "TRUE")
 
 
 class _RFC2136Client:
@@ -103,13 +105,15 @@ class _RFC2136Client:
     Encapsulates all communication with the target DNS server.
     """
     def __init__(self, server: str, port: int, key_name: str, key_secret: str,
-                 key_algorithm: dns.name.Name, timeout: int = DEFAULT_NETWORK_TIMEOUT) -> None:
+                 key_algorithm: dns.name.Name, sign_query: bool,
+                 timeout: int = DEFAULT_NETWORK_TIMEOUT) -> None:
         self.server = server
         self.port = port
         self.keyring = dns.tsigkeyring.from_text({
             key_name: key_secret
         })
         self.algorithm = key_algorithm
+        self.sign_query = sign_query
         self._default_timeout = timeout
 
     def add_txt_record(self, record_name: str, record_content: str, record_ttl: int) -> None:
@@ -217,8 +221,9 @@ def _query_soa(self, domain_name: str) -> bool:
         request = dns.message.make_query(domain, dns.rdatatype.SOA, dns.rdataclass.IN)
         # Turn off Recursion Desired bit in query
         request.flags ^= dns.flags.RD
-        # Use our TSIG keyring
-        request.use_tsig(self.keyring, algorithm=self.algorithm)
+        # Use our TSIG keyring if configured
+        if self.sign_query:
+            request.use_tsig(self.keyring, algorithm=self.algorithm)
 
         try:
             try:


Test patch: