Merge tag 'upstream/2.1.0'
Upstream version 2.1.0
Harlan Lieberman-Berg
5 months ago
1 | 1 | include README.rst |
2 | 2 | recursive-include docs * |
3 | 3 | recursive-include tests * |
4 | include certbot_dns_rfc2136/py.typed | |
4 | 5 | global-exclude __pycache__ |
5 | 6 | global-exclude *.py[cod] |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: certbot-dns-rfc2136 |
2 | Version: 1.21.0 | |
2 | Version: 2.1.0 | |
3 | 3 | Summary: RFC 2136 DNS Authenticator plugin for Certbot |
4 | 4 | Home-page: https://github.com/certbot/certbot |
5 | 5 | Author: Certbot Project |
6 | 6 | Author-email: certbot-dev@eff.org |
7 | 7 | License: Apache License 2.0 |
8 | Platform: UNKNOWN | |
9 | 8 | Classifier: Development Status :: 5 - Production/Stable |
10 | 9 | Classifier: Environment :: Plugins |
11 | 10 | Classifier: Intended Audience :: System Administrators |
13 | 12 | Classifier: Operating System :: POSIX :: Linux |
14 | 13 | Classifier: Programming Language :: Python |
15 | 14 | Classifier: Programming Language :: Python :: 3 |
16 | Classifier: Programming Language :: Python :: 3.6 | |
17 | 15 | Classifier: Programming Language :: Python :: 3.7 |
18 | 16 | Classifier: Programming Language :: Python :: 3.8 |
19 | 17 | Classifier: Programming Language :: Python :: 3.9 |
18 | Classifier: Programming Language :: Python :: 3.10 | |
19 | Classifier: Programming Language :: Python :: 3.11 | |
20 | 20 | Classifier: Topic :: Internet :: WWW/HTTP |
21 | 21 | Classifier: Topic :: Security |
22 | 22 | Classifier: Topic :: System :: Installation/Setup |
23 | 23 | Classifier: Topic :: System :: Networking |
24 | 24 | Classifier: Topic :: System :: Systems Administration |
25 | 25 | Classifier: Topic :: Utilities |
26 | Requires-Python: >=3.6 | |
26 | Requires-Python: >=3.7 | |
27 | 27 | Provides-Extra: docs |
28 | 28 | License-File: LICENSE.txt |
29 | ||
30 | UNKNOWN | |
31 |
64 | 64 | including for renewal, and cannot be silenced except by addressing the issue |
65 | 65 | (e.g., by using a command like ``chmod 600`` to restrict access to the file). |
66 | 66 | |
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 | ||
110 | 67 | Examples |
111 | 68 | -------- |
112 | 69 | |
138 | 95 | --dns-rfc2136-propagation-seconds 30 \\ |
139 | 96 | -d example.com |
140 | 97 | |
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 | ||
141 | 196 | """ |
0 | 0 | """DNS Authenticator using RFC 2136 Dynamic Updates.""" |
1 | 1 | import logging |
2 | from typing import Any | |
3 | from typing import Callable | |
2 | 4 | from typing import Optional |
3 | 5 | |
4 | 6 | import dns.flags |
41 | 43 | description = 'Obtain certificates using a DNS TXT record (if you are using BIND for DNS).' |
42 | 44 | ttl = 120 |
43 | 45 | |
44 | def __init__(self, *args, **kwargs): | |
46 | def __init__(self, *args: Any, **kwargs: Any) -> None: | |
45 | 47 | super().__init__(*args, **kwargs) |
46 | 48 | self.credentials: Optional[CredentialsConfiguration] = None |
47 | 49 | |
48 | 50 | @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: | |
50 | 53 | super().add_parser_arguments(add, default_propagation_seconds=60) |
51 | 54 | add('credentials', help='RFC 2136 credentials INI file.') |
52 | 55 | |
53 | def more_info(self): # pylint: disable=missing-function-docstring | |
56 | def more_info(self) -> str: | |
54 | 57 | return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \ |
55 | 58 | 'RFC 2136 Dynamic Updates.' |
56 | 59 | |
57 | def _validate_credentials(self, credentials): | |
60 | def _validate_credentials(self, credentials: CredentialsConfiguration) -> None: | |
58 | 61 | server = credentials.conf('server') |
59 | 62 | if not is_ipaddress(server): |
60 | 63 | raise errors.PluginError("The configured target DNS server ({0}) is not a valid IPv4 " |
64 | 67 | if not self.ALGORITHMS.get(algorithm.upper()): |
65 | 68 | raise errors.PluginError("Unknown algorithm: {0}.".format(algorithm)) |
66 | 69 | |
67 | def _setup_credentials(self): | |
70 | def _setup_credentials(self) -> None: | |
68 | 71 | self.credentials = self._configure_credentials( |
69 | 72 | 'credentials', |
70 | 73 | 'RFC 2136 credentials INI file', |
76 | 79 | self._validate_credentials |
77 | 80 | ) |
78 | 81 | |
79 | def _perform(self, _domain, validation_name, validation): | |
82 | def _perform(self, _domain: str, validation_name: str, validation: str) -> None: | |
80 | 83 | self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl) |
81 | 84 | |
82 | def _cleanup(self, _domain, validation_name, validation): | |
85 | def _cleanup(self, _domain: str, validation_name: str, validation: str) -> None: | |
83 | 86 | self._get_rfc2136_client().del_txt_record(validation_name, validation) |
84 | 87 | |
85 | def _get_rfc2136_client(self): | |
88 | def _get_rfc2136_client(self) -> "_RFC2136Client": | |
86 | 89 | if not self.credentials: # pragma: no cover |
87 | 90 | raise errors.Error("Plugin has not been prepared.") |
88 | 91 | return _RFC2136Client(self.credentials.conf('server'), |
97 | 100 | """ |
98 | 101 | Encapsulates all communication with the target DNS server. |
99 | 102 | """ |
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: | |
102 | 105 | self.server = server |
103 | 106 | self.port = port |
104 | 107 | self.keyring = dns.tsigkeyring.from_text({ |
107 | 110 | self.algorithm = key_algorithm |
108 | 111 | self._default_timeout = timeout |
109 | 112 | |
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: | |
111 | 114 | """ |
112 | 115 | Add a TXT record using the supplied information. |
113 | 116 | |
134 | 137 | except Exception as e: |
135 | 138 | raise errors.PluginError('Encountered error adding TXT record: {0}' |
136 | 139 | .format(e)) |
137 | rcode = response.rcode() | |
140 | rcode = response.rcode() # type: ignore[attr-defined] | |
138 | 141 | |
139 | 142 | if rcode == dns.rcode.NOERROR: |
140 | 143 | logger.debug('Successfully added TXT record %s', record_name) |
142 | 145 | raise errors.PluginError('Received response from server: {0}' |
143 | 146 | .format(dns.rcode.to_text(rcode))) |
144 | 147 | |
145 | def del_txt_record(self, record_name, record_content): | |
148 | def del_txt_record(self, record_name: str, record_content: str) -> None: | |
146 | 149 | """ |
147 | 150 | Delete a TXT record using the supplied information. |
148 | 151 | |
169 | 172 | except Exception as e: |
170 | 173 | raise errors.PluginError('Encountered error deleting TXT record: {0}' |
171 | 174 | .format(e)) |
172 | rcode = response.rcode() | |
175 | rcode = response.rcode() # type: ignore[attr-defined] | |
173 | 176 | |
174 | 177 | if rcode == dns.rcode.NOERROR: |
175 | 178 | logger.debug('Successfully deleted TXT record %s', record_name) |
177 | 180 | raise errors.PluginError('Received response from server: {0}' |
178 | 181 | .format(dns.rcode.to_text(rcode))) |
179 | 182 | |
180 | def _find_domain(self, record_name): | |
183 | def _find_domain(self, record_name: str) -> str: | |
181 | 184 | """ |
182 | 185 | Find the closest domain with an SOA record for a given domain name. |
183 | 186 | |
197 | 200 | raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.' |
198 | 201 | .format(record_name, domain_name_guesses)) |
199 | 202 | |
200 | def _query_soa(self, domain_name): | |
203 | def _query_soa(self, domain_name: str) -> bool: | |
201 | 204 | """ |
202 | 205 | Query a domain name for an authoritative SOA record. |
203 | 206 | |
212 | 215 | request = dns.message.make_query(domain, dns.rdatatype.SOA, dns.rdataclass.IN) |
213 | 216 | # Turn off Recursion Desired bit in query |
214 | 217 | request.flags ^= dns.flags.RD |
218 | # Use our TSIG keyring | |
219 | request.use_tsig(self.keyring, algorithm=self.algorithm) # type: ignore[attr-defined] | |
215 | 220 | |
216 | 221 | try: |
217 | 222 | try: |
219 | 224 | except (OSError, dns.exception.Timeout) as e: |
220 | 225 | logger.debug('TCP query failed, fallback to UDP: %s', e) |
221 | 226 | response = dns.query.udp(request, self.server, self._default_timeout, self.port) |
222 | rcode = response.rcode() | |
227 | rcode = response.rcode() # type: ignore[attr-defined] | |
223 | 228 | |
224 | 229 | # 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): | |
227 | 234 | logger.debug('Received authoritative SOA response for %s', domain_name) |
228 | 235 | return True |
229 | 236 |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: certbot-dns-rfc2136 |
2 | Version: 1.21.0 | |
2 | Version: 2.1.0 | |
3 | 3 | Summary: RFC 2136 DNS Authenticator plugin for Certbot |
4 | 4 | Home-page: https://github.com/certbot/certbot |
5 | 5 | Author: Certbot Project |
6 | 6 | Author-email: certbot-dev@eff.org |
7 | 7 | License: Apache License 2.0 |
8 | Platform: UNKNOWN | |
9 | 8 | Classifier: Development Status :: 5 - Production/Stable |
10 | 9 | Classifier: Environment :: Plugins |
11 | 10 | Classifier: Intended Audience :: System Administrators |
13 | 12 | Classifier: Operating System :: POSIX :: Linux |
14 | 13 | Classifier: Programming Language :: Python |
15 | 14 | Classifier: Programming Language :: Python :: 3 |
16 | Classifier: Programming Language :: Python :: 3.6 | |
17 | 15 | Classifier: Programming Language :: Python :: 3.7 |
18 | 16 | Classifier: Programming Language :: Python :: 3.8 |
19 | 17 | Classifier: Programming Language :: Python :: 3.9 |
18 | Classifier: Programming Language :: Python :: 3.10 | |
19 | Classifier: Programming Language :: Python :: 3.11 | |
20 | 20 | Classifier: Topic :: Internet :: WWW/HTTP |
21 | 21 | Classifier: Topic :: Security |
22 | 22 | Classifier: Topic :: System :: Installation/Setup |
23 | 23 | Classifier: Topic :: System :: Networking |
24 | 24 | Classifier: Topic :: System :: Systems Administration |
25 | 25 | Classifier: Topic :: Utilities |
26 | Requires-Python: >=3.6 | |
26 | Requires-Python: >=3.7 | |
27 | 27 | Provides-Extra: docs |
28 | 28 | License-File: LICENSE.txt |
29 | ||
30 | UNKNOWN | |
31 |
0 | 0 | LICENSE.txt |
1 | 1 | MANIFEST.in |
2 | 2 | README.rst |
3 | setup.cfg | |
4 | 3 | setup.py |
5 | 4 | certbot_dns_rfc2136/__init__.py |
5 | certbot_dns_rfc2136/py.typed | |
6 | 6 | certbot_dns_rfc2136.egg-info/PKG-INFO |
7 | 7 | certbot_dns_rfc2136.egg-info/SOURCES.txt |
8 | 8 | certbot_dns_rfc2136.egg-info/dependency_links.txt |
0 | 0 | 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 | |
4 | 4 | |
5 | 5 | [docs] |
6 | 6 | Sphinx>=1.0 |
176 | 176 | intersphinx_mapping = { |
177 | 177 | 'python': ('https://docs.python.org/', None), |
178 | 178 | '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), | |
180 | 180 | } |
3 | 3 | from setuptools import find_packages |
4 | 4 | from setuptools import setup |
5 | 5 | |
6 | version = '1.21.0' | |
6 | version = '2.1.0' | |
7 | 7 | |
8 | 8 | install_requires = [ |
9 | 9 | 'dnspython>=1.15.0', |
10 | 'setuptools>=39.0.1', | |
10 | 'setuptools>=41.6.0', | |
11 | 11 | ] |
12 | 12 | |
13 | 13 | if not os.environ.get('SNAP_BUILD'): |
37 | 37 | author="Certbot Project", |
38 | 38 | author_email='certbot-dev@eff.org', |
39 | 39 | license='Apache License 2.0', |
40 | python_requires='>=3.6', | |
40 | python_requires='>=3.7', | |
41 | 41 | classifiers=[ |
42 | 42 | 'Development Status :: 5 - Production/Stable', |
43 | 43 | 'Environment :: Plugins', |
46 | 46 | 'Operating System :: POSIX :: Linux', |
47 | 47 | 'Programming Language :: Python', |
48 | 48 | 'Programming Language :: Python :: 3', |
49 | 'Programming Language :: Python :: 3.6', | |
50 | 49 | 'Programming Language :: Python :: 3.7', |
51 | 50 | 'Programming Language :: Python :: 3.8', |
52 | 51 | 'Programming Language :: Python :: 3.9', |
52 | 'Programming Language :: Python :: 3.10', | |
53 | 'Programming Language :: Python :: 3.11', | |
53 | 54 | 'Topic :: Internet :: WWW/HTTP', |
54 | 55 | 'Topic :: Security', |
55 | 56 | 'Topic :: System :: Installation/Setup', |
0 | 0 | """Tests for certbot_dns_rfc2136._internal.dns_rfc2136.""" |
1 | 1 | |
2 | 2 | import unittest |
3 | from unittest import mock | |
3 | 4 | |
4 | 5 | import dns.flags |
5 | 6 | import dns.rcode |
6 | 7 | import dns.tsig |
7 | try: | |
8 | import mock | |
9 | except ImportError: # pragma: no cover | |
10 | from unittest import mock # type: ignore | |
11 | 8 | |
12 | 9 | from certbot import errors |
13 | 10 | from certbot.compat import os |
22 | 19 | VALID_CONFIG = {"rfc2136_server": SERVER, "rfc2136_name": NAME, "rfc2136_secret": SECRET} |
23 | 20 | TIMEOUT = 45 |
24 | 21 | |
22 | ||
25 | 23 | class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest): |
26 | 24 | |
27 | 25 | def setUp(self): |
112 | 110 | self.rfc2136_client.add_txt_record("bar", "baz", 42) |
113 | 111 | |
114 | 112 | 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])) | |
116 | 114 | |
117 | 115 | @mock.patch("dns.query.tcp") |
118 | 116 | def test_add_txt_record_wraps_errors(self, query_mock): |
145 | 143 | self.rfc2136_client.del_txt_record("bar", "baz") |
146 | 144 | |
147 | 145 | 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])) | |
149 | 147 | |
150 | 148 | @mock.patch("dns.query.tcp") |
151 | 149 | def test_del_txt_record_wraps_errors(self, query_mock): |