diff --git a/MANIFEST.in b/MANIFEST.in index 5a661ce..2e902de 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,5 +2,6 @@ include README.rst recursive-include docs * recursive-include tests * +include certbot_dns_rfc2136/py.typed global-exclude __pycache__ global-exclude *.py[cod] diff --git a/PKG-INFO b/PKG-INFO index 0a66b71..70f1139 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,12 +1,11 @@ Metadata-Version: 2.1 Name: certbot-dns-rfc2136 -Version: 1.21.0 +Version: 2.1.0 Summary: RFC 2136 DNS Authenticator plugin for Certbot Home-page: https://github.com/certbot/certbot Author: Certbot Project Author-email: certbot-dev@eff.org License: Apache License 2.0 -Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Plugins Classifier: Intended Audience :: System Administrators @@ -14,19 +13,17 @@ Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup Classifier: Topic :: System :: Networking Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities -Requires-Python: >=3.6 +Requires-Python: >=3.7 Provides-Extra: docs License-File: LICENSE.txt - -UNKNOWN - diff --git a/certbot_dns_rfc2136/__init__.py b/certbot_dns_rfc2136/__init__.py index 077a731..19734d2 100644 --- a/certbot_dns_rfc2136/__init__.py +++ b/certbot_dns_rfc2136/__init__.py @@ -65,49 +65,6 @@ including for renewal, and cannot be silenced except by addressing the issue (e.g., by using a command like ``chmod 600`` to restrict access to the file). -Sample BIND configuration -''''''''''''''''''''''''' - -Here's a sample BIND configuration for Certbot to use. You will need to -generate a new TSIG key, include it in the BIND configuration and grant it -permission to issue updates on the target DNS zone. - -.. code-block:: bash - :caption: Generate a new SHA512 TSIG key - - dnssec-keygen -a HMAC-SHA512 -b 512 -n HOST keyname. - -.. note:: - There are a few tools shipped with BIND that can all generate TSIG keys; - ``dnssec-keygen``, ``rndc-confgen``, and ``ddns-confgen``. Try and use the - most secure algorithm supported by your DNS server. - -.. code-block:: none - :caption: Sample BIND configuration - - key "keyname." { - algorithm hmac-sha512; - secret "4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \ -AmKd7ak51vWKgSl12ib86oQRPkpDjg=="; - }; - - zone "example.com." IN { - type master; - file "named.example.com"; - update-policy { - grant keyname. name _acme-challenge.example.com. txt; - }; - }; - -.. note:: - This configuration limits the scope of the TSIG key to just be able to - add and remove TXT records for one specific host for the purpose of - completing the ``dns-01`` challenge. If your version of BIND doesn't - support the ``update-policy`` directive, then you can use the less-secure - ``allow-update`` directive instead. `See the BIND documentation - `_ - for details. - Examples -------- @@ -139,4 +96,102 @@ --dns-rfc2136-propagation-seconds 30 \\ -d example.com + +Sample BIND configuration +''''''''''''''''''''''''' + +Here's a sample BIND configuration for Certbot to use. You will need to +generate a new TSIG key, include it in the BIND configuration and grant it +permission to issue updates on the target DNS zone. + +.. code-block:: bash + :caption: Generate a new SHA512 TSIG key + + tsig-keygen -a HMAC-SHA512 keyname. + +.. note:: + Prior to BIND version 9.10.0, you will need to use ``dnssec-keygen`` to generate + TSIG keys. Try and use the most secure algorithm supported by your DNS server. + +.. code-block:: none + :caption: Sample BIND configuration + + key "keyname." { + algorithm hmac-sha512; + secret "4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \ +AmKd7ak51vWKgSl12ib86oQRPkpDjg=="; + }; + + zone "example.com." IN { + type master; + file "named.example.com"; + update-policy { + grant keyname. name _acme-challenge.example.com. txt; + }; + }; + +.. note:: + This configuration limits the scope of the TSIG key to just be able to + add and remove TXT records for one specific host for the purpose of + completing the ``dns-01`` challenge. If your version of BIND doesn't + support the ``update-policy`` directive, then you can use the less-secure + ``allow-update`` directive instead. `See the BIND documentation + `_ + for details. + +Special considerations for multiple views in BIND +''''''''''''''''''''''''''''''''''''''''''''''''' + +If your BIND configuration leverages multiple views, Certbot may fail with an +``Unable to determine base domain for _acme-challenge.example.com`` error. +This error occurs when Certbot isn't able to communicate with an authorative +nameserver for the zone, one that answers with the AA (Authorative Answer) flag +set in the response. + +A common multiple view configuration with two views, external and internal, +can cause this error. If the zone is only present in the external view, and +the credentials_ ``dns_rfc2136_server`` setting is set (e.g. 127.0.0.1) so the +DNS server's ``match-clients`` view option causes the DNS server to route +Certbot's query to the internal view; the internal view doesn't contain the +zone, so the response won't have the AA flag set. + +One solution is to logically place the zone into the view Certbot is sending +queries to, with an +`in-view `_ +zone option. The zone will be then visible in both zones with exactly the same content. + +.. note:: + Order matters in BIND views, the ``in-view`` zone option must refer to a + view defined preceeding it, it cannot refer to a view defined later in the configuration file. + +.. code-block:: none + :caption: Split-view BIND configuration + + 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; }; + + view "external" { + match-clients { !internal-addresses; any; }; + + zone "example.com." IN { + type master; + file "named.example.com"; + update-policy { + grant keyname. name _acme-challenge.example.com. txt; + }; + }; + }; + + view "internal" { + zone "example.com." IN { + in-view external; + }; + }; + """ diff --git a/certbot_dns_rfc2136/_internal/dns_rfc2136.py b/certbot_dns_rfc2136/_internal/dns_rfc2136.py index 98687e6..2c52486 100644 --- a/certbot_dns_rfc2136/_internal/dns_rfc2136.py +++ b/certbot_dns_rfc2136/_internal/dns_rfc2136.py @@ -1,5 +1,7 @@ """DNS Authenticator using RFC 2136 Dynamic Updates.""" import logging +from typing import Any +from typing import Callable from typing import Optional import dns.flags @@ -42,20 +44,21 @@ description = 'Obtain certificates using a DNS TXT record (if you are using BIND for DNS).' ttl = 120 - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.credentials: Optional[CredentialsConfiguration] = None @classmethod - def add_parser_arguments(cls, add): # pylint: disable=arguments-differ + def add_parser_arguments(cls, add: Callable[..., None], + default_propagation_seconds: int = 60) -> None: super().add_parser_arguments(add, default_propagation_seconds=60) add('credentials', help='RFC 2136 credentials INI file.') - def more_info(self): # pylint: disable=missing-function-docstring + def more_info(self) -> str: return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ 'RFC 2136 Dynamic Updates.' - def _validate_credentials(self, credentials): + def _validate_credentials(self, credentials: CredentialsConfiguration) -> None: server = credentials.conf('server') if not is_ipaddress(server): raise errors.PluginError("The configured target DNS server ({0}) is not a valid IPv4 " @@ -65,7 +68,7 @@ if not self.ALGORITHMS.get(algorithm.upper()): raise errors.PluginError("Unknown algorithm: {0}.".format(algorithm)) - def _setup_credentials(self): + def _setup_credentials(self) -> None: self.credentials = self._configure_credentials( 'credentials', 'RFC 2136 credentials INI file', @@ -77,13 +80,13 @@ self._validate_credentials ) - def _perform(self, _domain, validation_name, validation): + def _perform(self, _domain: str, validation_name: str, validation: str) -> None: self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl) - def _cleanup(self, _domain, validation_name, validation): + def _cleanup(self, _domain: str, validation_name: str, validation: str) -> None: self._get_rfc2136_client().del_txt_record(validation_name, validation) - def _get_rfc2136_client(self): + def _get_rfc2136_client(self) -> "_RFC2136Client": if not self.credentials: # pragma: no cover raise errors.Error("Plugin has not been prepared.") return _RFC2136Client(self.credentials.conf('server'), @@ -98,8 +101,8 @@ """ Encapsulates all communication with the target DNS server. """ - def __init__(self, server, port, key_name, key_secret, key_algorithm, - timeout=DEFAULT_NETWORK_TIMEOUT): + def __init__(self, server: str, port: int, key_name: str, key_secret: str, + key_algorithm: dns.name.Name, timeout: int = DEFAULT_NETWORK_TIMEOUT) -> None: self.server = server self.port = port self.keyring = dns.tsigkeyring.from_text({ @@ -108,7 +111,7 @@ self.algorithm = key_algorithm self._default_timeout = timeout - def add_txt_record(self, record_name, record_content, record_ttl): + def add_txt_record(self, record_name: str, record_content: str, record_ttl: int) -> None: """ Add a TXT record using the supplied information. @@ -135,7 +138,7 @@ except Exception as e: raise errors.PluginError('Encountered error adding TXT record: {0}' .format(e)) - rcode = response.rcode() + rcode = response.rcode() # type: ignore[attr-defined] if rcode == dns.rcode.NOERROR: logger.debug('Successfully added TXT record %s', record_name) @@ -143,7 +146,7 @@ raise errors.PluginError('Received response from server: {0}' .format(dns.rcode.to_text(rcode))) - def del_txt_record(self, record_name, record_content): + def del_txt_record(self, record_name: str, record_content: str) -> None: """ Delete a TXT record using the supplied information. @@ -170,7 +173,7 @@ except Exception as e: raise errors.PluginError('Encountered error deleting TXT record: {0}' .format(e)) - rcode = response.rcode() + rcode = response.rcode() # type: ignore[attr-defined] if rcode == dns.rcode.NOERROR: logger.debug('Successfully deleted TXT record %s', record_name) @@ -178,7 +181,7 @@ raise errors.PluginError('Received response from server: {0}' .format(dns.rcode.to_text(rcode))) - def _find_domain(self, record_name): + def _find_domain(self, record_name: str) -> str: """ Find the closest domain with an SOA record for a given domain name. @@ -198,7 +201,7 @@ raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.' .format(record_name, domain_name_guesses)) - def _query_soa(self, domain_name): + def _query_soa(self, domain_name: str) -> bool: """ Query a domain name for an authoritative SOA record. @@ -213,6 +216,8 @@ 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) # type: ignore[attr-defined] try: try: @@ -220,11 +225,13 @@ except (OSError, dns.exception.Timeout) as e: logger.debug('TCP query failed, fallback to UDP: %s', e) response = dns.query.udp(request, self.server, self._default_timeout, self.port) - rcode = response.rcode() + rcode = response.rcode() # type: ignore[attr-defined] # Authoritative Answer bit should be set - if (rcode == dns.rcode.NOERROR and response.get_rrset(response.answer, - domain, dns.rdataclass.IN, dns.rdatatype.SOA) and response.flags & dns.flags.AA): + if (rcode == dns.rcode.NOERROR + and response.get_rrset(response.answer, # type: ignore[attr-defined] + domain, dns.rdataclass.IN, dns.rdatatype.SOA) + and response.flags & dns.flags.AA): logger.debug('Received authoritative SOA response for %s', domain_name) return True diff --git a/certbot_dns_rfc2136/py.typed b/certbot_dns_rfc2136/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/certbot_dns_rfc2136.egg-info/PKG-INFO b/certbot_dns_rfc2136.egg-info/PKG-INFO index 0a66b71..70f1139 100644 --- a/certbot_dns_rfc2136.egg-info/PKG-INFO +++ b/certbot_dns_rfc2136.egg-info/PKG-INFO @@ -1,12 +1,11 @@ Metadata-Version: 2.1 Name: certbot-dns-rfc2136 -Version: 1.21.0 +Version: 2.1.0 Summary: RFC 2136 DNS Authenticator plugin for Certbot Home-page: https://github.com/certbot/certbot Author: Certbot Project Author-email: certbot-dev@eff.org License: Apache License 2.0 -Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Plugins Classifier: Intended Audience :: System Administrators @@ -14,19 +13,17 @@ Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Classifier: Topic :: System :: Installation/Setup Classifier: Topic :: System :: Networking Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities -Requires-Python: >=3.6 +Requires-Python: >=3.7 Provides-Extra: docs License-File: LICENSE.txt - -UNKNOWN - diff --git a/certbot_dns_rfc2136.egg-info/SOURCES.txt b/certbot_dns_rfc2136.egg-info/SOURCES.txt index d497f0c..5356ffc 100644 --- a/certbot_dns_rfc2136.egg-info/SOURCES.txt +++ b/certbot_dns_rfc2136.egg-info/SOURCES.txt @@ -1,9 +1,9 @@ LICENSE.txt MANIFEST.in README.rst -setup.cfg setup.py certbot_dns_rfc2136/__init__.py +certbot_dns_rfc2136/py.typed certbot_dns_rfc2136.egg-info/PKG-INFO certbot_dns_rfc2136.egg-info/SOURCES.txt certbot_dns_rfc2136.egg-info/dependency_links.txt diff --git a/certbot_dns_rfc2136.egg-info/entry_points.txt b/certbot_dns_rfc2136.egg-info/entry_points.txt index 4dd75d0..9f8b39d 100644 --- a/certbot_dns_rfc2136.egg-info/entry_points.txt +++ b/certbot_dns_rfc2136.egg-info/entry_points.txt @@ -1,3 +1,2 @@ [certbot.plugins] dns-rfc2136 = certbot_dns_rfc2136._internal.dns_rfc2136:Authenticator - diff --git a/certbot_dns_rfc2136.egg-info/requires.txt b/certbot_dns_rfc2136.egg-info/requires.txt index 94ab74a..cab83b7 100644 --- a/certbot_dns_rfc2136.egg-info/requires.txt +++ b/certbot_dns_rfc2136.egg-info/requires.txt @@ -1,7 +1,7 @@ dnspython>=1.15.0 -setuptools>=39.0.1 -acme>=1.21.0 -certbot>=1.21.0 +setuptools>=41.6.0 +acme>=2.1.0 +certbot>=2.1.0 [docs] Sphinx>=1.0 diff --git a/docs/conf.py b/docs/conf.py index 782f494..70b888b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -177,5 +177,5 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/', None), 'acme': ('https://acme-python.readthedocs.org/en/latest/', None), - 'certbot': ('https://certbot.eff.org/docs/', None), + 'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None), } diff --git a/setup.cfg b/setup.cfg index adf5ed7..8bfd5a1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal = 1 - [egg_info] tag_build = tag_date = 0 diff --git a/setup.py b/setup.py index 7e105e4..0dc8e79 100644 --- a/setup.py +++ b/setup.py @@ -4,11 +4,11 @@ from setuptools import find_packages from setuptools import setup -version = '1.21.0' +version = '2.1.0' install_requires = [ 'dnspython>=1.15.0', - 'setuptools>=39.0.1', + 'setuptools>=41.6.0', ] if not os.environ.get('SNAP_BUILD'): @@ -38,7 +38,7 @@ author="Certbot Project", author_email='certbot-dev@eff.org', license='Apache License 2.0', - python_requires='>=3.6', + python_requires='>=3.7', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Plugins', @@ -47,10 +47,11 @@ 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', 'Topic :: System :: Installation/Setup', diff --git a/tests/dns_rfc2136_test.py b/tests/dns_rfc2136_test.py index 72ea6d9..1f91d3c 100644 --- a/tests/dns_rfc2136_test.py +++ b/tests/dns_rfc2136_test.py @@ -1,14 +1,11 @@ """Tests for certbot_dns_rfc2136._internal.dns_rfc2136.""" import unittest +from unittest import mock import dns.flags import dns.rcode import dns.tsig -try: - import mock -except ImportError: # pragma: no cover - from unittest import mock # type: ignore from certbot import errors from certbot.compat import os @@ -23,6 +20,7 @@ VALID_CONFIG = {"rfc2136_server": SERVER, "rfc2136_name": NAME, "rfc2136_secret": SECRET} TIMEOUT = 45 + class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): def setUp(self): @@ -113,7 +111,7 @@ self.rfc2136_client.add_txt_record("bar", "baz", 42) query_mock.assert_called_with(mock.ANY, SERVER, TIMEOUT, PORT) - self.assertTrue("bar. 42 IN TXT \"baz\"" in str(query_mock.call_args[0][0])) + self.assertIn('bar. 42 IN TXT "baz"', str(query_mock.call_args[0][0])) @mock.patch("dns.query.tcp") def test_add_txt_record_wraps_errors(self, query_mock): @@ -146,7 +144,7 @@ self.rfc2136_client.del_txt_record("bar", "baz") query_mock.assert_called_with(mock.ANY, SERVER, TIMEOUT, PORT) - self.assertTrue("bar. 0 NONE TXT \"baz\"" in str(query_mock.call_args[0][0])) + self.assertIn('bar. 0 NONE TXT "baz"', str(query_mock.call_args[0][0])) @mock.patch("dns.query.tcp") def test_del_txt_record_wraps_errors(self, query_mock):