Codebase list python-certbot-dns-rfc2136 / 8708642
New upstream version 2.1.0 Harlan Lieberman-Berg 1 year, 4 months ago
13 changed file(s) with 147 addition(s) and 95 deletion(s). Raw diff Collapse all Expand all
11 include README.rst
22 recursive-include docs *
33 recursive-include tests *
4 include certbot_dns_rfc2136/py.typed
45 global-exclude __pycache__
56 global-exclude *.py[cod]
00 Metadata-Version: 2.1
11 Name: certbot-dns-rfc2136
2 Version: 1.21.0
2 Version: 2.1.0
33 Summary: RFC 2136 DNS Authenticator plugin for Certbot
44 Home-page: https://github.com/certbot/certbot
55 Author: Certbot Project
66 Author-email: certbot-dev@eff.org
77 License: Apache License 2.0
8 Platform: UNKNOWN
98 Classifier: Development Status :: 5 - Production/Stable
109 Classifier: Environment :: Plugins
1110 Classifier: Intended Audience :: System Administrators
1312 Classifier: Operating System :: POSIX :: Linux
1413 Classifier: Programming Language :: Python
1514 Classifier: Programming Language :: Python :: 3
16 Classifier: Programming Language :: Python :: 3.6
1715 Classifier: Programming Language :: Python :: 3.7
1816 Classifier: Programming Language :: Python :: 3.8
1917 Classifier: Programming Language :: Python :: 3.9
18 Classifier: Programming Language :: Python :: 3.10
19 Classifier: Programming Language :: Python :: 3.11
2020 Classifier: Topic :: Internet :: WWW/HTTP
2121 Classifier: Topic :: Security
2222 Classifier: Topic :: System :: Installation/Setup
2323 Classifier: Topic :: System :: Networking
2424 Classifier: Topic :: System :: Systems Administration
2525 Classifier: Topic :: Utilities
26 Requires-Python: >=3.6
26 Requires-Python: >=3.7
2727 Provides-Extra: docs
2828 License-File: LICENSE.txt
29
30 UNKNOWN
31
6464 including for renewal, and cannot be silenced except by addressing the issue
6565 (e.g., by using a command like ``chmod 600`` to restrict access to the file).
6666
67 Sample BIND configuration
68 '''''''''''''''''''''''''
69
70 Here's a sample BIND configuration for Certbot to use. You will need to
71 generate a new TSIG key, include it in the BIND configuration and grant it
72 permission to issue updates on the target DNS zone.
73
74 .. code-block:: bash
75 :caption: Generate a new SHA512 TSIG key
76
77 dnssec-keygen -a HMAC-SHA512 -b 512 -n HOST keyname.
78
79 .. note::
80 There are a few tools shipped with BIND that can all generate TSIG keys;
81 ``dnssec-keygen``, ``rndc-confgen``, and ``ddns-confgen``. Try and use the
82 most secure algorithm supported by your DNS server.
83
84 .. code-block:: none
85 :caption: Sample BIND configuration
86
87 key "keyname." {
88 algorithm hmac-sha512;
89 secret "4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \
90 AmKd7ak51vWKgSl12ib86oQRPkpDjg==";
91 };
92
93 zone "example.com." IN {
94 type master;
95 file "named.example.com";
96 update-policy {
97 grant keyname. name _acme-challenge.example.com. txt;
98 };
99 };
100
101 .. note::
102 This configuration limits the scope of the TSIG key to just be able to
103 add and remove TXT records for one specific host for the purpose of
104 completing the ``dns-01`` challenge. If your version of BIND doesn't
105 support the ``update-policy`` directive, then you can use the less-secure
106 ``allow-update`` directive instead. `See the BIND documentation
107 <https://bind9.readthedocs.io/en/latest/reference.html#dynamic-update-policies>`_
108 for details.
109
11067 Examples
11168 --------
11269
13895 --dns-rfc2136-propagation-seconds 30 \\
13996 -d example.com
14097
98
99 Sample BIND configuration
100 '''''''''''''''''''''''''
101
102 Here's a sample BIND configuration for Certbot to use. You will need to
103 generate a new TSIG key, include it in the BIND configuration and grant it
104 permission to issue updates on the target DNS zone.
105
106 .. code-block:: bash
107 :caption: Generate a new SHA512 TSIG key
108
109 tsig-keygen -a HMAC-SHA512 keyname.
110
111 .. note::
112 Prior to BIND version 9.10.0, you will need to use ``dnssec-keygen`` to generate
113 TSIG keys. Try and use the most secure algorithm supported by your DNS server.
114
115 .. code-block:: none
116 :caption: Sample BIND configuration
117
118 key "keyname." {
119 algorithm hmac-sha512;
120 secret "4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \
121 AmKd7ak51vWKgSl12ib86oQRPkpDjg==";
122 };
123
124 zone "example.com." IN {
125 type master;
126 file "named.example.com";
127 update-policy {
128 grant keyname. name _acme-challenge.example.com. txt;
129 };
130 };
131
132 .. note::
133 This configuration limits the scope of the TSIG key to just be able to
134 add and remove TXT records for one specific host for the purpose of
135 completing the ``dns-01`` challenge. If your version of BIND doesn't
136 support the ``update-policy`` directive, then you can use the less-secure
137 ``allow-update`` directive instead. `See the BIND documentation
138 <https://bind9.readthedocs.io/en/latest/reference.html#dynamic-update-policies>`_
139 for details.
140
141 Special considerations for multiple views in BIND
142 '''''''''''''''''''''''''''''''''''''''''''''''''
143
144 If your BIND configuration leverages multiple views, Certbot may fail with an
145 ``Unable to determine base domain for _acme-challenge.example.com`` error.
146 This error occurs when Certbot isn't able to communicate with an authorative
147 nameserver for the zone, one that answers with the AA (Authorative Answer) flag
148 set in the response.
149
150 A common multiple view configuration with two views, external and internal,
151 can cause this error. If the zone is only present in the external view, and
152 the credentials_ ``dns_rfc2136_server`` setting is set (e.g. 127.0.0.1) so the
153 DNS server's ``match-clients`` view option causes the DNS server to route
154 Certbot's query to the internal view; the internal view doesn't contain the
155 zone, so the response won't have the AA flag set.
156
157 One solution is to logically place the zone into the view Certbot is sending
158 queries to, with an
159 `in-view <https://bind9.readthedocs.io/en/latest/reference.html#multiple-views>`_
160 zone option. The zone will be then visible in both zones with exactly the same content.
161
162 .. note::
163 Order matters in BIND views, the ``in-view`` zone option must refer to a
164 view defined preceeding it, it cannot refer to a view defined later in the configuration file.
165
166 .. code-block:: none
167 :caption: Split-view BIND configuration
168
169 key "keyname." {
170 algorithm hmac-sha512;
171 secret "4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \
172 AmKd7ak51vWKgSl12ib86oQRPkpDjg==";
173 };
174
175 // adjust internal-addresses to suit your needs
176 acl internal-address { 127.0.0.0/8; 10.0.0.0/8; 192.168.0.0/16; 172.16.0.0/12; };
177
178 view "external" {
179 match-clients { !internal-addresses; any; };
180
181 zone "example.com." IN {
182 type master;
183 file "named.example.com";
184 update-policy {
185 grant keyname. name _acme-challenge.example.com. txt;
186 };
187 };
188 };
189
190 view "internal" {
191 zone "example.com." IN {
192 in-view external;
193 };
194 };
195
141196 """
00 """DNS Authenticator using RFC 2136 Dynamic Updates."""
11 import logging
2 from typing import Any
3 from typing import Callable
24 from typing import Optional
35
46 import dns.flags
4143 description = 'Obtain certificates using a DNS TXT record (if you are using BIND for DNS).'
4244 ttl = 120
4345
44 def __init__(self, *args, **kwargs):
46 def __init__(self, *args: Any, **kwargs: Any) -> None:
4547 super().__init__(*args, **kwargs)
4648 self.credentials: Optional[CredentialsConfiguration] = None
4749
4850 @classmethod
49 def add_parser_arguments(cls, add): # pylint: disable=arguments-differ
51 def add_parser_arguments(cls, add: Callable[..., None],
52 default_propagation_seconds: int = 60) -> None:
5053 super().add_parser_arguments(add, default_propagation_seconds=60)
5154 add('credentials', help='RFC 2136 credentials INI file.')
5255
53 def more_info(self): # pylint: disable=missing-function-docstring
56 def more_info(self) -> str:
5457 return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
5558 'RFC 2136 Dynamic Updates.'
5659
57 def _validate_credentials(self, credentials):
60 def _validate_credentials(self, credentials: CredentialsConfiguration) -> None:
5861 server = credentials.conf('server')
5962 if not is_ipaddress(server):
6063 raise errors.PluginError("The configured target DNS server ({0}) is not a valid IPv4 "
6467 if not self.ALGORITHMS.get(algorithm.upper()):
6568 raise errors.PluginError("Unknown algorithm: {0}.".format(algorithm))
6669
67 def _setup_credentials(self):
70 def _setup_credentials(self) -> None:
6871 self.credentials = self._configure_credentials(
6972 'credentials',
7073 'RFC 2136 credentials INI file',
7679 self._validate_credentials
7780 )
7881
79 def _perform(self, _domain, validation_name, validation):
82 def _perform(self, _domain: str, validation_name: str, validation: str) -> None:
8083 self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)
8184
82 def _cleanup(self, _domain, validation_name, validation):
85 def _cleanup(self, _domain: str, validation_name: str, validation: str) -> None:
8386 self._get_rfc2136_client().del_txt_record(validation_name, validation)
8487
85 def _get_rfc2136_client(self):
88 def _get_rfc2136_client(self) -> "_RFC2136Client":
8689 if not self.credentials: # pragma: no cover
8790 raise errors.Error("Plugin has not been prepared.")
8891 return _RFC2136Client(self.credentials.conf('server'),
97100 """
98101 Encapsulates all communication with the target DNS server.
99102 """
100 def __init__(self, server, port, key_name, key_secret, key_algorithm,
101 timeout=DEFAULT_NETWORK_TIMEOUT):
103 def __init__(self, server: str, port: int, key_name: str, key_secret: str,
104 key_algorithm: dns.name.Name, timeout: int = DEFAULT_NETWORK_TIMEOUT) -> None:
102105 self.server = server
103106 self.port = port
104107 self.keyring = dns.tsigkeyring.from_text({
107110 self.algorithm = key_algorithm
108111 self._default_timeout = timeout
109112
110 def add_txt_record(self, record_name, record_content, record_ttl):
113 def add_txt_record(self, record_name: str, record_content: str, record_ttl: int) -> None:
111114 """
112115 Add a TXT record using the supplied information.
113116
134137 except Exception as e:
135138 raise errors.PluginError('Encountered error adding TXT record: {0}'
136139 .format(e))
137 rcode = response.rcode()
140 rcode = response.rcode() # type: ignore[attr-defined]
138141
139142 if rcode == dns.rcode.NOERROR:
140143 logger.debug('Successfully added TXT record %s', record_name)
142145 raise errors.PluginError('Received response from server: {0}'
143146 .format(dns.rcode.to_text(rcode)))
144147
145 def del_txt_record(self, record_name, record_content):
148 def del_txt_record(self, record_name: str, record_content: str) -> None:
146149 """
147150 Delete a TXT record using the supplied information.
148151
169172 except Exception as e:
170173 raise errors.PluginError('Encountered error deleting TXT record: {0}'
171174 .format(e))
172 rcode = response.rcode()
175 rcode = response.rcode() # type: ignore[attr-defined]
173176
174177 if rcode == dns.rcode.NOERROR:
175178 logger.debug('Successfully deleted TXT record %s', record_name)
177180 raise errors.PluginError('Received response from server: {0}'
178181 .format(dns.rcode.to_text(rcode)))
179182
180 def _find_domain(self, record_name):
183 def _find_domain(self, record_name: str) -> str:
181184 """
182185 Find the closest domain with an SOA record for a given domain name.
183186
197200 raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.'
198201 .format(record_name, domain_name_guesses))
199202
200 def _query_soa(self, domain_name):
203 def _query_soa(self, domain_name: str) -> bool:
201204 """
202205 Query a domain name for an authoritative SOA record.
203206
212215 request = dns.message.make_query(domain, dns.rdatatype.SOA, dns.rdataclass.IN)
213216 # Turn off Recursion Desired bit in query
214217 request.flags ^= dns.flags.RD
218 # Use our TSIG keyring
219 request.use_tsig(self.keyring, algorithm=self.algorithm) # type: ignore[attr-defined]
215220
216221 try:
217222 try:
219224 except (OSError, dns.exception.Timeout) as e:
220225 logger.debug('TCP query failed, fallback to UDP: %s', e)
221226 response = dns.query.udp(request, self.server, self._default_timeout, self.port)
222 rcode = response.rcode()
227 rcode = response.rcode() # type: ignore[attr-defined]
223228
224229 # Authoritative Answer bit should be set
225 if (rcode == dns.rcode.NOERROR and response.get_rrset(response.answer,
226 domain, dns.rdataclass.IN, dns.rdatatype.SOA) and response.flags & dns.flags.AA):
230 if (rcode == dns.rcode.NOERROR
231 and response.get_rrset(response.answer, # type: ignore[attr-defined]
232 domain, dns.rdataclass.IN, dns.rdatatype.SOA)
233 and response.flags & dns.flags.AA):
227234 logger.debug('Received authoritative SOA response for %s', domain_name)
228235 return True
229236
00 Metadata-Version: 2.1
11 Name: certbot-dns-rfc2136
2 Version: 1.21.0
2 Version: 2.1.0
33 Summary: RFC 2136 DNS Authenticator plugin for Certbot
44 Home-page: https://github.com/certbot/certbot
55 Author: Certbot Project
66 Author-email: certbot-dev@eff.org
77 License: Apache License 2.0
8 Platform: UNKNOWN
98 Classifier: Development Status :: 5 - Production/Stable
109 Classifier: Environment :: Plugins
1110 Classifier: Intended Audience :: System Administrators
1312 Classifier: Operating System :: POSIX :: Linux
1413 Classifier: Programming Language :: Python
1514 Classifier: Programming Language :: Python :: 3
16 Classifier: Programming Language :: Python :: 3.6
1715 Classifier: Programming Language :: Python :: 3.7
1816 Classifier: Programming Language :: Python :: 3.8
1917 Classifier: Programming Language :: Python :: 3.9
18 Classifier: Programming Language :: Python :: 3.10
19 Classifier: Programming Language :: Python :: 3.11
2020 Classifier: Topic :: Internet :: WWW/HTTP
2121 Classifier: Topic :: Security
2222 Classifier: Topic :: System :: Installation/Setup
2323 Classifier: Topic :: System :: Networking
2424 Classifier: Topic :: System :: Systems Administration
2525 Classifier: Topic :: Utilities
26 Requires-Python: >=3.6
26 Requires-Python: >=3.7
2727 Provides-Extra: docs
2828 License-File: LICENSE.txt
29
30 UNKNOWN
31
00 LICENSE.txt
11 MANIFEST.in
22 README.rst
3 setup.cfg
43 setup.py
54 certbot_dns_rfc2136/__init__.py
5 certbot_dns_rfc2136/py.typed
66 certbot_dns_rfc2136.egg-info/PKG-INFO
77 certbot_dns_rfc2136.egg-info/SOURCES.txt
88 certbot_dns_rfc2136.egg-info/dependency_links.txt
00 [certbot.plugins]
11 dns-rfc2136 = certbot_dns_rfc2136._internal.dns_rfc2136:Authenticator
2
00 dnspython>=1.15.0
1 setuptools>=39.0.1
2 acme>=1.21.0
3 certbot>=1.21.0
1 setuptools>=41.6.0
2 acme>=2.1.0
3 certbot>=2.1.0
44
55 [docs]
66 Sphinx>=1.0
176176 intersphinx_mapping = {
177177 'python': ('https://docs.python.org/', None),
178178 'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
179 'certbot': ('https://certbot.eff.org/docs/', None),
179 'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),
180180 }
0 [bdist_wheel]
1 universal = 1
2
30 [egg_info]
41 tag_build =
52 tag_date = 0
33 from setuptools import find_packages
44 from setuptools import setup
55
6 version = '1.21.0'
6 version = '2.1.0'
77
88 install_requires = [
99 'dnspython>=1.15.0',
10 'setuptools>=39.0.1',
10 'setuptools>=41.6.0',
1111 ]
1212
1313 if not os.environ.get('SNAP_BUILD'):
3737 author="Certbot Project",
3838 author_email='certbot-dev@eff.org',
3939 license='Apache License 2.0',
40 python_requires='>=3.6',
40 python_requires='>=3.7',
4141 classifiers=[
4242 'Development Status :: 5 - Production/Stable',
4343 'Environment :: Plugins',
4646 'Operating System :: POSIX :: Linux',
4747 'Programming Language :: Python',
4848 'Programming Language :: Python :: 3',
49 'Programming Language :: Python :: 3.6',
5049 'Programming Language :: Python :: 3.7',
5150 'Programming Language :: Python :: 3.8',
5251 'Programming Language :: Python :: 3.9',
52 'Programming Language :: Python :: 3.10',
53 'Programming Language :: Python :: 3.11',
5354 'Topic :: Internet :: WWW/HTTP',
5455 'Topic :: Security',
5556 'Topic :: System :: Installation/Setup',
00 """Tests for certbot_dns_rfc2136._internal.dns_rfc2136."""
11
22 import unittest
3 from unittest import mock
34
45 import dns.flags
56 import dns.rcode
67 import dns.tsig
7 try:
8 import mock
9 except ImportError: # pragma: no cover
10 from unittest import mock # type: ignore
118
129 from certbot import errors
1310 from certbot.compat import os
2219 VALID_CONFIG = {"rfc2136_server": SERVER, "rfc2136_name": NAME, "rfc2136_secret": SECRET}
2320 TIMEOUT = 45
2421
22
2523 class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):
2624
2725 def setUp(self):
112110 self.rfc2136_client.add_txt_record("bar", "baz", 42)
113111
114112 query_mock.assert_called_with(mock.ANY, SERVER, TIMEOUT, PORT)
115 self.assertTrue("bar. 42 IN TXT \"baz\"" in str(query_mock.call_args[0][0]))
113 self.assertIn('bar. 42 IN TXT "baz"', str(query_mock.call_args[0][0]))
116114
117115 @mock.patch("dns.query.tcp")
118116 def test_add_txt_record_wraps_errors(self, query_mock):
145143 self.rfc2136_client.del_txt_record("bar", "baz")
146144
147145 query_mock.assert_called_with(mock.ANY, SERVER, TIMEOUT, PORT)
148 self.assertTrue("bar. 0 NONE TXT \"baz\"" in str(query_mock.call_args[0][0]))
146 self.assertIn('bar. 0 NONE TXT "baz"', str(query_mock.call_args[0][0]))
149147
150148 @mock.patch("dns.query.tcp")
151149 def test_del_txt_record_wraps_errors(self, query_mock):