Repository: certbot/certbot (language: python)

Issue:
Usability improvements for dns-rfc2136 plugin
[This community thread points out](https://community.letsencrypt.org/t/supported-bind9-dns-challenge-client/96542) two issues with the current plugin:

 - The name is confusing. Most people don't know what RFC2136 is. We should probably add an alias like `dns-bind` or `dns-nsupdate` (after the widely used CLI update tool).

 - [The instructions to use dnssec-keygen](https://certbot-dns-rfc2136.readthedocs.io/en/stable/#sample-bind-configuration) should be replaced with tsig-keygen for Bind 9.13+. Though @joohoi pointed out on chat that most distros are still on Bind 9.11 so far.


Discussion / hints:
bind is not the only server providing this service based on RFC2136.
PowerDNS should also support this.
dns-dyndns might be confusing as well as dyndns is also used for other servicees where someone can update only it's own record. mostly through specific other tools.
We've made a lot of changes to Certbot since this issue was opened. If you still have this issue with an up-to-date version of Certbot, can you please add a comment letting us know? This helps us to better see what issues are still affecting our users. If there is no activity in the next 30 days, this issue will be automatically closed.

\*pokes bot\*

Fix patch:
diff --git a/AUTHORS.md b/AUTHORS.md
index f8708dde1a0..4b8dd9e732b 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -210,6 +210,7 @@ Authors
 * [Peter Conrad](https://github.com/pconrad-fb)
 * [Peter Eckersley](https://github.com/pde)
 * [Peter Mosmans](https://github.com/PeterMosmans)
+* [Phil Martin](https://github.com/frillip)
 * [Philippe Langlois](https://github.com/langloisjp)
 * [Philipp Spitzer](https://github.com/spitza)
 * [Piero Steinger](https://github.com/Jadaw1n)
diff --git a/acme/acme/messages.py b/acme/acme/messages.py
index 6c4e23815a1..0e02a054e23 100644
--- a/acme/acme/messages.py
+++ b/acme/acme/messages.py
@@ -141,7 +141,7 @@ class Error(jose.JSONObjectWithFields, errors.Error):
     # Mypy does not understand the josepy magic happening here, and falsely claims
     # that subproblems is redefined. Let's ignore the type check here.
     @subproblems.decoder  # type: ignore
-    def subproblems(value: List[Dict[str, Any]]) -> Tuple['Error', ...]:  # type: ignore[misc]  # pylint: disable=no-self-argument,missing-function-docstring
+    def subproblems(value: List[Dict[str, Any]]) -> Tuple['Error', ...]:  # pylint: disable=no-self-argument,missing-function-docstring
         return tuple(Error.from_json(subproblem) for subproblem in value)
 
     @classmethod
@@ -546,7 +546,7 @@ class Authorization(ResourceBody):
     # Mypy does not understand the josepy magic happening here, and falsely claims
     # that challenge is redefined. Let's ignore the type check here.
     @challenges.decoder  # type: ignore
-    def challenges(value: List[Dict[str, Any]]) -> Tuple[ChallengeBody, ...]:  # type: ignore[misc]  # pylint: disable=no-self-argument,missing-function-docstring
+    def challenges(value: List[Dict[str, Any]]) -> Tuple[ChallengeBody, ...]:  # pylint: disable=no-self-argument,missing-function-docstring
         return tuple(ChallengeBody.from_json(chall) for chall in value)
 
 
@@ -629,7 +629,7 @@ class Order(ResourceBody):
     # Mypy does not understand the josepy magic happening here, and falsely claims
     # that identifiers is redefined. Let's ignore the type check here.
     @identifiers.decoder  # type: ignore
-    def identifiers(value: List[Dict[str, Any]]) -> Tuple[Identifier, ...]:  # type: ignore[misc]  # pylint: disable=no-self-argument,missing-function-docstring
+    def identifiers(value: List[Dict[str, Any]]) -> Tuple[Identifier, ...]:  # pylint: disable=no-self-argument,missing-function-docstring
         return tuple(Identifier.from_json(identifier) for identifier in value)
 
 
diff --git a/acme/docs/jws-help.txt b/acme/docs/jws-help.txt
index bfd16dff4d7..34cf5ce2356 100644
--- a/acme/docs/jws-help.txt
+++ b/acme/docs/jws-help.txt
@@ -3,6 +3,6 @@ usage: jws [-h] [--compact] {sign,verify} ...
 positional arguments:
   {sign,verify}
 
-options:
+optional arguments:
   -h, --help     show this help message and exit
   --compact
diff --git a/certbot-apache/certbot_apache/_internal/apache_util.py b/certbot-apache/certbot_apache/_internal/apache_util.py
index a538d8f0d3f..54b9f9824e0 100644
--- a/certbot-apache/certbot_apache/_internal/apache_util.py
+++ b/certbot-apache/certbot_apache/_internal/apache_util.py
@@ -136,20 +136,18 @@ def included_in_paths(filepath: str, paths: Iterable[str]) -> bool:
     return any(fnmatch.fnmatch(filepath, path) for path in paths)
 
 
-def parse_defines(apachectl: str) -> Dict[str, str]:
+def parse_defines(define_cmd: List[str]) -> Dict[str, str]:
     """
     Gets Defines from httpd process and returns a dictionary of
     the defined variables.
 
-    :param str apachectl: Path to apachectl executable
+    :param list define_cmd: httpd command to dump defines
 
     :returns: dictionary of defined variables
     :rtype: dict
     """
 
     variables: Dict[str, str] = {}
-    define_cmd = [apachectl, "-t", "-D",
-                  "DUMP_RUN_CFG"]
     matches = parse_from_subprocess(define_cmd, r"Define: ([^ \n]*)")
     try:
         matches.remove("DUMP_RUN_CFG")
@@ -165,33 +163,31 @@ def parse_defines(apachectl: str) -> Dict[str, str]:
     return variables
 
 
-def parse_includes(apachectl: str) -> List[str]:
+def parse_includes(inc_cmd: List[str]) -> List[str]:
     """
     Gets Include directives from httpd process and returns a list of
     their values.
 
-    :param str apachectl: Path to apachectl executable
+    :param list inc_cmd: httpd command to dump includes
 
     :returns: list of found Include directive values
     :rtype: list of str
     """
 
-    inc_cmd: List[str] = [apachectl, "-t", "-D", "DUMP_INCLUDES"]
     return parse_from_subprocess(inc_cmd, r"\(.*\) (.*)")
 
 
-def parse_modules(apachectl: str) -> List[str]:
+def parse_modules(mod_cmd: List[str]) -> List[str]:
     """
     Get loaded modules from httpd process, and return the list
     of loaded module names.
 
-    :param str apachectl: Path to apachectl executable
+    :param list mod_cmd: httpd command to dump loaded modules
 
     :returns: list of found LoadModule module names
     :rtype: list of str
     """
 
-    mod_cmd = [apachectl, "-t", "-D", "DUMP_MODULES"]
     return parse_from_subprocess(mod_cmd, r"(.*)_module")
 
 
diff --git a/certbot-apache/certbot_apache/_internal/apacheparser.py b/certbot-apache/certbot_apache/_internal/apacheparser.py
index 0aba0cb3c0d..8d2a79754b0 100644
--- a/certbot-apache/certbot_apache/_internal/apacheparser.py
+++ b/certbot-apache/certbot_apache/_internal/apacheparser.py
@@ -118,7 +118,8 @@ def add_child_block(self, name: str, parameters: Optional[List[str]] = None,
 
     # pylint: disable=unused-argument
     def add_child_directive(self, name: str, parameters: Optional[List[str]] = None,
-                            position: int = None) -> ApacheDirectiveNode:  # pragma: no cover
+                            position: Optional[int] = None
+    ) -> ApacheDirectiveNode:  # pragma: no cover
         """Adds a new DirectiveNode to the sequence of children"""
         new_dir = ApacheDirectiveNode(name=assertions.PASS,
                                       parameters=assertions.PASS,
diff --git a/certbot-apache/certbot_apache/_internal/configurator.py b/certbot-apache/certbot_apache/_internal/configurator.py
index 4c68c5ae8f7..41209deb497 100644
--- a/certbot-apache/certbot_apache/_internal/configurator.py
+++ b/certbot-apache/certbot_apache/_internal/configurator.py
@@ -85,6 +85,10 @@ def __init__(self,
         self.restart_cmd = ['apache2ctl', 'graceful'] if not restart_cmd else restart_cmd
         self.restart_cmd_alt = restart_cmd_alt
         self.conftest_cmd = ['apache2ctl', 'configtest'] if not conftest_cmd else conftest_cmd
+        syntax_tests_cmd_base = [ctl, '-t', '-D']
+        self.get_defines_cmd = syntax_tests_cmd_base + ['DUMP_RUN_CFG']
+        self.get_includes_cmd = syntax_tests_cmd_base + ['DUMP_INCLUDES']
+        self.get_modules_cmd = syntax_tests_cmd_base + ['DUMP_MODULES']
         self.enmod = enmod
         self.dismod = dismod
         self.le_vhost_ext = le_vhost_ext
@@ -166,6 +170,17 @@ def pick_apache_config(self, warn_on_no_mod_ssl: bool = True) -> str:
             return apache_util.find_ssl_apache_conf("old")
         return apache_util.find_ssl_apache_conf("current")
 
+    def _override_cmds(self) -> None:
+        """
+        Set our various command binaries to whatever the user has overridden for apachectl
+        """
+        self.options.version_cmd[0] = self.options.ctl
+        self.options.restart_cmd[0] = self.options.ctl
+        self.options.conftest_cmd[0] = self.options.ctl
+        self.options.get_modules_cmd[0] = self.options.ctl
+        self.options.get_includes_cmd[0] = self.options.ctl
+        self.options.get_defines_cmd[0] = self.options.ctl
+
     def _prepare_options(self) -> None:
         """
         Set the values possibly changed by command line parameters to
@@ -181,10 +196,7 @@ def _prepare_options(self) -> None:
             else:
                 setattr(self.options, o, getattr(self.OS_DEFAULTS, o))
 
-        # Special cases
-        self.options.version_cmd[0] = self.options.ctl
-        self.options.restart_cmd[0] = self.options.ctl
-        self.options.conftest_cmd[0] = self.options.ctl
+        self._override_cmds()
 
     @classmethod
     def add_parser_arguments(cls, add: Callable[..., None]) -> None:
@@ -476,9 +488,9 @@ def get_parsernode_root(self, metadata: Dict[str, Any]) -> dualparser.DualBlockN
 
         if HAS_APACHECONFIG:
             apache_vars = {
-                "defines": apache_util.parse_defines(self.options.ctl),
-                "includes": apache_util.parse_includes(self.options.ctl),
-                "modules": apache_util.parse_modules(self.options.ctl),
+                "defines": apache_util.parse_defines(self.options.get_defines_cmd),
+                "includes": apache_util.parse_includes(self.options.get_includes_cmd),
+                "modules": apache_util.parse_modules(self.options.get_modules_cmd),
             }
             metadata["apache_vars"] = apache_vars
 
@@ -800,7 +812,7 @@ def find_best_http_vhost(
         return self._find_best_vhost(target, filtered_vhosts, filter_defaults)
 
     def _find_best_vhost(
-        self, target_name: str, vhosts: List[obj.VirtualHost] = None,
+        self, target_name: str, vhosts: Optional[List[obj.VirtualHost]] = None,
         filter_defaults: bool = True
     ) -> Optional[obj.VirtualHost]:
         """Finds the best vhost for a target_name.
diff --git a/certbot-apache/certbot_apache/_internal/override_centos.py b/certbot-apache/certbot_apache/_internal/override_centos.py
index f9de9fad97b..de5c312680c 100644
--- a/certbot-apache/certbot_apache/_internal/override_centos.py
+++ b/certbot-apache/certbot_apache/_internal/override_centos.py
@@ -22,6 +22,7 @@ class CentOSConfigurator(configurator.ApacheConfigurator):
         vhost_files="*.conf",
         logs_root="/var/log/httpd",
         ctl="apachectl",
+        apache_bin="httpd",
         version_cmd=['apachectl', '-v'],
         restart_cmd=['apachectl', 'graceful'],
         restart_cmd_alt=['apachectl', 'restart'],
@@ -48,6 +49,37 @@ def config_test(self) -> None:
             else:
                 raise
 
+    def _rhel9_or_newer(self) -> bool:
+        os_name, os_version = util.get_os_info()
+        rhel_derived = os_name in [
+            "centos", "centos linux",
+            "cloudlinux",
+            "ol", "oracle",
+            "rhel", "redhatenterpriseserver", "red hat enterprise linux server",
+            "scientific", "scientific linux",
+        ]
+        at_least_v9 = util.parse_loose_version(os_version) >= util.parse_loose_version('9')
+        return rhel_derived and at_least_v9
+
+    def _override_cmds(self) -> None:
+        super()._override_cmds()
+
+        # As of RHEL 9, apachectl can't be passed flags like "-v" or "-t -D", so
+        # instead use options.bin (i.e. httpd) for version_cmd and the various
+        # get_X commands
+        if self._rhel9_or_newer():
+            if not self.options.bin:
+                raise ValueError("OS option apache_bin must be set for CentOS") # pragma: no cover
+
+            self.options.version_cmd[0] = self.options.bin
+            self.options.get_modules_cmd[0] = self.options.bin
+            self.options.get_includes_cmd[0] = self.options.bin
+            self.options.get_defines_cmd[0] = self.options.bin
+
+        if not self.options.restart_cmd_alt:  # pragma: no cover
+            raise ValueError("OS option restart_cmd_alt must be set for CentOS.")
+        self.options.restart_cmd_alt[0] = self.options.ctl
+
     def _try_restart_fedora(self) -> None:
         """
         Tries to restart httpd using systemctl to generate the self signed key pair.
@@ -61,16 +93,6 @@ def _try_restart_fedora(self) -> None:
         # Finish with actual config check to see if systemctl restart helped
         super().config_test()
 
-    def _prepare_options(self) -> None:
-        """
-        Override the options dictionary initialization in order to support
-        alternative restart cmd used in CentOS.
-        """
-        super()._prepare_options()
-        if not self.options.restart_cmd_alt:  # pragma: no cover
-            raise ValueError("OS option restart_cmd_alt must be set for CentOS.")
-        self.options.restart_cmd_alt[0] = self.options.ctl
-
     def get_parser(self) -> "CentOSParser":
         """Initializes the ApacheParser"""
         return CentOSParser(
diff --git a/certbot-apache/certbot_apache/_internal/parser.py b/certbot-apache/certbot_apache/_internal/parser.py
index b122979e4b3..9805ad78172 100644
--- a/certbot-apache/certbot_apache/_internal/parser.py
+++ b/certbot-apache/certbot_apache/_internal/parser.py
@@ -297,7 +297,7 @@ def update_runtime_variables(self) -> None:
 
     def update_defines(self) -> None:
         """Updates the dictionary of known variables in the configuration"""
-        self.variables = apache_util.parse_defines(self.configurator.options.ctl)
+        self.variables = apache_util.parse_defines(self.configurator.options.get_defines_cmd)
 
     def update_includes(self) -> None:
         """Get includes from httpd process, and add them to DOM if needed"""
@@ -307,7 +307,7 @@ def update_includes(self) -> None:
         # configuration files
         _ = self.find_dir("Include")
 
-        matches = apache_util.parse_includes(self.configurator.options.ctl)
+        matches = apache_util.parse_includes(self.configurator.options.get_includes_cmd)
         if matches:
             for i in matches:
                 if not self.parsed_in_current(i):
@@ -316,7 +316,7 @@ def update_includes(self) -> None:
     def update_modules(self) -> None:
         """Get loaded modules from httpd process, and add them to DOM"""
 
-        matches = apache_util.parse_modules(self.configurator.options.ctl)
+        matches = apache_util.parse_modules(self.configurator.options.get_modules_cmd)
         for mod in matches:
             self.add_mod(mod.strip())
 
diff --git a/certbot-ci/setup.py b/certbot-ci/setup.py
index 4341ef266d5..728c6c70786 100644
--- a/certbot-ci/setup.py
+++ b/certbot-ci/setup.py
@@ -15,7 +15,6 @@
 install_requires = [
     'coverage',
     'cryptography',
-    'docker-compose',
     'pyopenssl',
     'pytest',
     'pytest-cov',
diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py
index e06cef5a765..19734d29b84 100644
--- a/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py
+++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py
@@ -107,12 +107,11 @@
 .. code-block:: bash
    :caption: Generate a new SHA512 TSIG key
 
-   dnssec-keygen -a HMAC-SHA512 -b 512 -n HOST keyname.
+   tsig-keygen -a HMAC-SHA512 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.
+   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
diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
index 8cf6d99660d..2c52486e217 100644
--- a/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
+++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
@@ -216,6 +216,8 @@ 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) # type: ignore[attr-defined]
 
         try:
             try:
diff --git a/certbot-nginx/certbot_nginx/_internal/configurator.py b/certbot-nginx/certbot_nginx/_internal/configurator.py
index 39784110cf4..2786e5e8e30 100644
--- a/certbot-nginx/certbot_nginx/_internal/configurator.py
+++ b/certbot-nginx/certbot_nginx/_internal/configurator.py
@@ -1112,7 +1112,7 @@ def auth_hint(self,  # pragma: no cover
     ###################################################
     # Wrapper functions for Reverter class (Installer)
     ###################################################
-    def save(self, title: str = None, temporary: bool = False) -> None:
+    def save(self, title: Optional[str] = None, temporary: bool = False) -> None:
         """Saves all changes to the configuration files.
 
         :param str title: The title of the save. If a title is given, the
diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md
index 231f5c45353..d74cb1cb20a 100644
--- a/certbot/CHANGELOG.md
+++ b/certbot/CHANGELOG.md
@@ -2,7 +2,7 @@
 
 Certbot adheres to [Semantic Versioning](https://semver.org/).
 
-## 1.32.0 - master
+## 1.33.0 - master
 
 ### Added
 
@@ -18,6 +18,23 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
 
 More details about these changes can be found on our GitHub repo.
 
+## 1.32.0 - 2022-11-08
+
+### Added
+
+*
+
+### Changed
+
+* DNS RFC2136 module now uses the TSIG key to check for an authoritative SOA record. Helps the use of split-horizon and multiple views in BIND9 using the key in an ACL to determine which view to use.
+
+### Fixed
+
+* CentOS 9 and other RHEL-derived OSes now correctly use httpd instead of apachectl for
+  various Apache-related commands
+
+More details about these changes can be found on our GitHub repo.
+
 ## 1.31.0 - 2022-10-04
 
 ### Added
diff --git a/certbot/certbot/_internal/account.py b/certbot/certbot/_internal/account.py
index 8ebc71de307..eb2466f3277 100644
--- a/certbot/certbot/_internal/account.py
+++ b/certbot/certbot/_internal/account.py
@@ -108,7 +108,7 @@ def __eq__(self, other: Any) -> bool:
 class AccountMemoryStorage(interfaces.AccountStorage):
     """In-memory account storage."""
 
-    def __init__(self, initial_accounts: Dict[str, Account] = None) -> None:
+    def __init__(self, initial_accounts: Optional[Dict[str, Account]] = None) -> None:
         self.accounts = initial_accounts if initial_accounts is not None else {}
 
     def find_all(self) -> List[Account]:
diff --git a/certbot/certbot/_internal/display/dummy_readline.py b/certbot/certbot/_internal/display/dummy_readline.py
index 2b6e4c31008..62152b050b4 100644
--- a/certbot/certbot/_internal/display/dummy_readline.py
+++ b/certbot/certbot/_internal/display/dummy_readline.py
@@ -11,6 +11,7 @@ def get_completer() -> Optional[Callable[[], str]]:
 
 def get_completer_delims() -> List[str]:
     """An empty implementation of readline.get_completer_delims."""
+    return []
 
 
 def parse_and_bind(unused_command: str) -> None:
diff --git a/certbot/certbot/_internal/main.py b/certbot/certbot/_internal/main.py
index 2ece8651760..d72fa4d069e 100644
--- a/certbot/certbot/_internal/main.py
+++ b/certbot/certbot/_internal/main.py
@@ -1683,7 +1683,7 @@ def make_displayer(config: configuration.NamespaceConfig
             devnull.close()
 
 
-def main(cli_args: List[str] = None) -> Optional[Union[str, int]]:
+def main(cli_args: Optional[List[str]] = None) -> Optional[Union[str, int]]:
     """Run Certbot.
 
     :param cli_args: command line to Certbot, defaults to ``sys.argv[1:]``
diff --git a/certbot/docs/cli-help.txt b/certbot/docs/cli-help.txt
index aadfb67cb5e..3c0538b729f 100644
--- a/certbot/docs/cli-help.txt
+++ b/certbot/docs/cli-help.txt
@@ -35,7 +35,7 @@ manage your account:
   --agree-tos       Agree to the ACME server's Subscriber Agreement
    -m EMAIL         Email address for important account notifications
 
-options:
+optional arguments:
   -h, --help            show this help message and exit
   -c CONFIG_FILE, --config CONFIG_FILE
                         path to config file (default: /etc/letsencrypt/cli.ini
@@ -126,7 +126,7 @@ options:
                         case, and to know when to deprecate support for past
                         Python versions and flags. If you wish to hide this
                         information from the Let's Encrypt server, set this to
-                        "". (default: CertbotACMEClient/1.31.0 (certbot;
+                        "". (default: CertbotACMEClient/1.32.0 (certbot;
                         OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY
                         (SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel).
                         The flags encoded in the user agent are: --duplicate,
diff --git a/certbot/docs/using.rst b/certbot/docs/using.rst
index 0e0da3d5c95..6218af59b49 100644
--- a/certbot/docs/using.rst
+++ b/certbot/docs/using.rst
@@ -321,6 +321,7 @@ njalla_            Y    N    DNS Authentication for njalla
 DuckDNS_           Y    N    DNS Authentication for DuckDNS
 Porkbun_           Y    N    DNS Authentication for Porkbun
 Infomaniak_        Y    N    DNS Authentication using Infomaniak Domains API
+dns-multi_         Y    N    DNS authentication of 100+ providers using go-acme/lego
 ================== ==== ==== ===============================================================
 
 .. _haproxy: https://github.com/greenhost/certbot-haproxy
@@ -343,6 +344,7 @@ Infomaniak_        Y    N    DNS Authentication using Infomaniak Domains API
 .. _DuckDNS: https://github.com/infinityofspace/certbot_dns_duckdns
 .. _Porkbun: https://github.com/infinityofspace/certbot_dns_porkbun
 .. _Infomaniak: https://github.com/Infomaniak/certbot-dns-infomaniak
+.. _dns-multi: https://github.com/alexzorin/certbot-dns-multi
 
 If you're interested, you can also :ref:`write your own plugin <dev-plugin>`.
 
diff --git a/certbot/setup.py b/certbot/setup.py
index 8ea0b0e6f30..9e7f8e4f348 100644
--- a/certbot/setup.py
+++ b/certbot/setup.py
@@ -67,7 +67,11 @@ def read_file(filename, encoding='utf8'):
     'ipdb',
     # poetry 1.2.0+ is required for it to pin pip, setuptools, and wheel. See
     # https://github.com/python-poetry/poetry/issues/1584.
-    'poetry>=1.2.0a1',
+    'poetry>=1.2.0',
+    # poetry-plugin-export>=1.1.0 is required to use the constraints.txt export
+    # format. See
+    # https://github.com/python-poetry/poetry-plugin-export/blob/efcfd34859e72f6a79a80398f197ce6eb2bbd7cd/CHANGELOG.md#added.
+    'poetry-plugin-export>=1.1.0',
     'twine',
 ]
 
diff --git a/tools/docker/deploy.sh b/tools/docker/deploy.sh
index c173c4158ec..9b04f3e49fc 100755
--- a/tools/docker/deploy.sh
+++ b/tools/docker/deploy.sh
@@ -46,6 +46,10 @@ ParseRequestedArch "${2}"
 TagAndPushForAllRequestedArch() {
     DOCKER_REPO="${DOCKER_HUB_ORG}/${1}"
     for TARGET_ARCH in "${ALL_REQUESTED_ARCH[@]}"; do
+        # NOTE: In early 2022, we were experiencing regular "docker push"
+        # timeouts, so we added these "--debug" flags to learn more. Since we
+        # added them, we haven't had another timeout, so until we experience
+        # another timeout & can get the deubg logs, we're leaving them in.
         docker --debug push "${DOCKER_REPO}:${TARGET_ARCH}-${TAG_BASE}"
 
         if [[ "${TAG_BASE}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
diff --git a/tools/oldest_constraints.txt b/tools/oldest_constraints.txt
index 2e723bd8151..9d7313959d7 100644
--- a/tools/oldest_constraints.txt
+++ b/tools/oldest_constraints.txt
@@ -1,110 +1,94 @@
 # This file was generated by tools/pinning/oldest/repin.sh and can be updated using
 # that script.
-apacheconfig==0.3.2
-asn1crypto==0.24.0
-astroid==2.11.7; python_version >= "3.7"
-atomicwrites==1.4.1; sys_platform == "win32" and python_version >= "3.7"
-attrs==22.1.0; python_version >= "3.7"
-bcrypt==3.2.2; python_version >= "3.7"
-boto3==1.15.15
-botocore==1.18.15
-cached-property==1.5.2; python_version >= "3.7"
-certifi==2022.6.15; python_version >= "3.7"
-cffi==1.9.1
-chardet==3.0.4
-cloudflare==1.5.1
-colorama==0.4.5; sys_platform == "win32" and python_version >= "3.7"
-configargparse==0.10.0
-configobj==5.0.6
-coverage==6.4.2; python_version >= "3.7"
-cryptography==3.2.1
-cython==0.29.31
-dill==0.3.5.1; python_version >= "3.7"
-distlib==0.3.5; python_version >= "3.7"
-distro==1.0.1
-dns-lexicon==3.2.1
-dnspython==1.15.0
-docker-compose==1.25.5; python_version >= "3.7"
-docker==4.2.2; python_version >= "3.7"
-dockerpty==0.4.1; python_version >= "3.7"
-docopt==0.6.2; python_version >= "3.7"
-execnet==1.9.0; python_version >= "3.7"
-filelock==3.7.1; python_version >= "3.7"
-funcsigs==0.4
-future==0.18.2; python_version >= "3.7"
-google-api-python-client==1.5.5
-httplib2==0.9.2
-idna==2.6
-importlib-metadata==4.12.0; python_version < "3.8" and python_version >= "3.7"
-iniconfig==1.1.1; python_version >= "3.7"
-ipaddress==1.0.16
-isort==5.10.1; python_version >= "3.7" and python_version < "4.0"
-jmespath==0.10.0; python_version >= "3.7"
-josepy==1.13.0; python_version >= "3.7"
-jsonschema==3.2.0; python_version >= "3.7"
-lazy-object-proxy==1.7.1; python_version >= "3.7"
-logger==1.4; python_version >= "3.7"
-mccabe==0.7.0; python_version >= "3.7"
-mypy-extensions==0.4.3; python_version >= "3.7"
-mypy==0.971; python_version >= "3.7"
-ndg-httpsclient==0.3.2
-oauth2client==4.0.0
-packaging==21.3; python_version >= "3.7"
-paramiko==2.11.0; python_version >= "3.7"
-parsedatetime==2.4
-pbr==1.8.0
-pip==22.2.1; python_version >= "3.7"
-platformdirs==2.5.2; python_version >= "3.7"
-pluggy==1.0.0; python_version >= "3.7"
-ply==3.4
-py==1.11.0; python_version >= "3.7"
-pyasn1-modules==0.0.10; python_version >= "3.7"
-pyasn1==0.1.9
-pycparser==2.14
-pylint==2.13.9
-pynacl==1.5.0; python_version >= "3.7"
-pyopenssl==17.5.0
-pyparsing==2.2.1
-pypiwin32==223; sys_platform == "win32" and python_version >= "3.7"
-pyrfc3339==1.0
-pyrsistent==0.18.1; python_version >= "3.7"
-pytest-cov==3.0.0; python_version >= "3.7"
-pytest-forked==1.4.0; python_version >= "3.7"
-pytest-xdist==2.5.0; python_version >= "3.7"
-pytest==7.1.2; python_version >= "3.7"
-python-augeas==0.5.0
-python-dateutil==2.8.2; python_version >= "3.7"
-python-digitalocean==1.11
-pytz==2019.3
-pywin32==304; sys_platform == "win32" and python_version >= "3.7"
-pyyaml==5.4.1; python_version >= "3.7"
-requests-file==1.5.1; python_version >= "3.7"
-requests-toolbelt==0.9.1; python_version >= "3.7"
-requests==2.20.0
-rsa==4.9; python_version >= "3.7" and python_version < "4"
-s3transfer==0.3.7; python_version >= "3.7"
-setuptools==41.6.0
-six==1.11.0
-texttable==1.6.4; python_version >= "3.7"
-tldextract==3.3.1; python_version >= "3.7"
-tomli==2.0.1; python_version < "3.11" and python_version >= "3.7" or python_full_version <= "3.11.0a6" and python_version >= "3.7" or python_version >= "3.7"
-tox==1.9.2; python_version >= "3.7"
-typed-ast==1.5.4; python_version >= "3.7" and python_version < "3.8" or implementation_name == "cpython" and python_version < "3.8" and python_version >= "3.7"
-types-cryptography==3.3.21; python_version >= "3.7"
-types-pyopenssl==22.0.9; python_version >= "3.7"
-types-pyrfc3339==1.1.1; python_version >= "3.7"
-types-python-dateutil==2.8.19; python_version >= "3.7"
-types-pytz==2022.1.2; python_version >= "3.7"
-types-requests==2.28.5; python_version >= "3.7"
-types-setuptools==63.2.2; python_version >= "3.7"
-types-six==1.16.18; python_version >= "3.7"
-types-urllib3==1.26.17; python_version >= "3.7"
-typing-extensions==4.3.0; python_version >= "3.7" or python_version < "3.10" and python_version >= "3.7" or python_version < "3.8" and python_version >= "3.7"
-uritemplate==3.0.1; python_version >= "3.7"
-urllib3==1.24.2
-virtualenv==20.16.2; python_version >= "3.7"
-websocket-client==0.59.0; python_version >= "3.7"
-wheel==0.33.6
-wheel==0.33.6; python_version >= "3.7"
-wrapt==1.14.1; python_version >= "3.7"
-zipp==3.8.1; python_version < "3.8" and python_version >= "3.7"
+apacheconfig==0.3.2 ; python_full_version < "3.8.0" and python_version >= "3.7"
+asn1crypto==0.24.0 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0"
+astroid==2.11.7 ; python_full_version < "3.8.0" and python_version >= "3.7"
+attrs==22.1.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
+boto3==1.15.15 ; python_full_version < "3.8.0" and python_version >= "3.7"
+botocore==1.18.15 ; python_full_version < "3.8.0" and python_version >= "3.7"
+certifi==2022.9.24 ; python_full_version < "3.8.0" and python_version >= "3.7"
+cffi==1.9.1 ; python_full_version < "3.8.0" and python_version >= "3.7"
+chardet==3.0.4 ; python_full_version < "3.8.0" and python_version >= "3.7"
+cloudflare==1.5.1 ; python_full_version < "3.8.0" and python_version >= "3.7"
+colorama==0.4.6 ; python_full_version < "3.8.0" and sys_platform == "win32" and python_version >= "3.7"
+configargparse==0.10.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
+configobj==5.0.6 ; python_full_version < "3.8.0" and python_version >= "3.7"
+coverage==6.5.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
+cryptography==3.2.1 ; python_full_version < "3.8.0" and python_version >= "3.7"
+cython==0.29.32 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0"
+dill==0.3.6 ; python_version >= "3.7" and python_full_version < "3.8.0"
+distlib==0.3.6 ; python_version >= "3.7" and python_full_version < "3.8.0"
+distro==1.0.1 ; python_full_version < "3.8.0" and python_version >= "3.7"
+dns-lexicon==3.2.1 ; python_full_version < "3.8.0" and python_version >= "3.7"
+dnspython==1.15.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
+exceptiongroup==1.0.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
+execnet==1.9.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
+filelock==3.8.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
+funcsigs==0.4 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0"
+future==0.18.2 ; python_full_version < "3.8.0" and python_version >= "3.7"
+google-api-python-client==1.5.5 ; python_full_version < "3.8.0" and python_version >= "3.7"
+httplib2==0.9.2 ; python_full_version < "3.8.0" and python_version >= "3.7"
+idna==2.6 ; python_full_version < "3.8.0" and python_version >= "3.7"
+importlib-metadata==5.0.0 ; python_version >= "3.7" and python_version < "3.8"
+iniconfig==1.1.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
+ipaddress==1.0.16 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0"
+isort==5.10.1 ; python_full_version < "3.8.0" and python_version >= "3.7"
+jmespath==0.10.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
+josepy==1.13.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
+lazy-object-proxy==1.8.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
+logger==1.4 ; python_full_version < "3.8.0" and python_version >= "3.7"
+mccabe==0.7.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
+mypy-extensions==0.4.3 ; python_version >= "3.7" and python_full_version < "3.8.0"
+mypy==0.990 ; python_version >= "3.7" and python_full_version < "3.8.0"
+ndg-httpsclient==0.3.2 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0"
+oauth2client==4.0.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
+packaging==21.3 ; python_version >= "3.7" and python_full_version < "3.8.0"
+parsedatetime==2.4 ; python_full_version < "3.8.0" and python_version >= "3.7"
+pbr==1.8.0 ; python_full_version >= "3.7.0" and python_full_version < "3.8.0"
+pip==22.3.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
+platformdirs==2.5.3 ; python_version >= "3.7" and python_full_version < "3.8.0"
+pluggy==1.0.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
+ply==3.4 ; python_full_version < "3.8.0" and python_version >= "3.7"
+py==1.11.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
+pyasn1-modules==0.0.10 ; python_full_version < "3.8.0" and python_version >= "3.7"
+pyasn1==0.1.9 ; python_full_version < "3.8.0" and python_version >= "3.7"
+pycparser==2.14 ; python_full_version < "3.8.0" and python_version >= "3.7"
+pylint==2.13.9 ; python_full_version < "3.8.0" and python_version >= "3.7"
+pyopenssl==17.5.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
+pyparsing==2.2.1 ; python_full_version < "3.8.0" and python_version >= "3.7"
+pyrfc3339==1.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
+pytest-cov==4.0.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
+pytest-xdist==3.0.2 ; python_version >= "3.7" and python_full_version < "3.8.0"
+pytest==7.2.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
+python-augeas==0.5.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
+python-dateutil==2.8.2 ; python_full_version < "3.8.0" and python_version >= "3.7"
+python-digitalocean==1.11 ; python_full_version < "3.8.0" and python_version >= "3.7"
+pytz==2019.3 ; python_full_version < "3.8.0" and python_version >= "3.7"
+pywin32==305 ; python_version >= "3.7" and python_full_version < "3.8.0" and sys_platform == "win32"
+pyyaml==6.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
+requests-file==1.5.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
+requests==2.20.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
+rsa==4.9 ; python_full_version < "3.8.0" and python_version >= "3.7"
+s3transfer==0.3.7 ; python_full_version < "3.8.0" and python_version >= "3.7"
+setuptools==41.6.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
+six==1.11.0 ; python_full_version < "3.8.0" and python_version >= "3.7"
+tldextract==3.4.0 ; python_version >= "3.7" and python_full_version < "3.8.0"
+tomli==2.0.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
+tox==1.9.2 ; python_version >= "3.7" and python_full_version < "3.8.0"
+typed-ast==1.5.4 ; python_version < "3.8" and python_version >= "3.7"
+types-cryptography==3.3.23.2 ; python_version >= "3.7" and python_full_version < "3.8.0"
+types-pyopenssl==22.1.0.2 ; python_version >= "3.7" and python_full_version < "3.8.0"
+types-pyrfc3339==1.1.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
+types-python-dateutil==2.8.19.3 ; python_version >= "3.7" and python_full_version < "3.8.0"
+types-pytz==2022.6.0.1 ; python_version >= "3.7" and python_full_version < "3.8.0"
+types-requests==2.28.11.4 ; python_version >= "3.7" and python_full_version < "3.8.0"
+types-setuptools==65.5.0.2 ; python_version >= "3.7" and python_full_version < "3.8.0"
+types-six==1.16.21.2 ; python_version >= "3.7" and python_full_version < "3.8.0"
+types-urllib3==1.26.25.3 ; python_version >= "3.7" and python_full_version < "3.8.0"
+typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "3.8"
+uritemplate==3.0.1 ; python_full_version < "3.8.0" and python_version >= "3.7"
+urllib3==1.24.2 ; python_full_version < "3.8.0" and python_version >= "3.7"
+virtualenv==20.16.6 ; python_version >= "3.7" and python_full_version < "3.8.0"
+wheel==0.33.6 ; python_full_version < "3.8.0" and python_version >= "3.7"
+wrapt==1.14.1 ; python_full_version < "3.8.0" and python_version >= "3.7"
+zipp==3.10.0 ; python_version >= "3.7" and python_version < "3.8"
diff --git a/tools/pinning/common/export-pinned-dependencies.sh b/tools/pinning/common/export-pinned-dependencies.sh
index be6f0aa4ef3..e1c93b0dc1a 100755
--- a/tools/pinning/common/export-pinned-dependencies.sh
+++ b/tools/pinning/common/export-pinned-dependencies.sh
@@ -37,8 +37,11 @@ if [ -f poetry.lock ]; then
     rm poetry.lock
 fi
 
-poetry lock >&2
+# If you're running this with different Python versions (say to update both our
+# "current" and "oldest" pinnings), poetry's cache can become corrupted causing
+# poetry to hang indefinitely. --no-cache avoids this.
+poetry lock --no-cache >&2
 trap 'rm poetry.lock' EXIT
 
 # We need to remove local packages from the output.
-poetry export --without-hashes | sed '/^acme @/d; /certbot/d;'
+poetry export --format constraints.txt --without-hashes | sed '/^acme @/d; /certbot/d;'
diff --git a/tools/pinning/current/pyproject.toml b/tools/pinning/current/pyproject.toml
index 77b4fdf8d07..d1ebe025ac7 100644
--- a/tools/pinning/current/pyproject.toml
+++ b/tools/pinning/current/pyproject.toml
@@ -50,18 +50,6 @@ awscli = ">=1.22.76"
 # as a dependency here to ensure a version of cython is pinned for extra
 # stability.
 cython = "*"
-# poetry 1.2.0+ is required for it to pin pip, setuptools, and wheel. See
-# https://github.com/python-poetry/poetry/issues/1584. This version is required
-# here in addition to certbot/setup.py because otherwise the pre-release
-# version of poetry will not be installed.
-#
-# Additionally, newer versions of poetry/poetry-core include package extras
-# (e.g. "docker[ssh]") in its `poetry export` output which is valid for
-# requirements files, but not constraints files. We are currently using that
-# output as constraints so let's also pin back poetry and poetry-core until we
-# have a better workaround.
-poetry = "1.2.0a2"
-poetry-core = "1.1.0a7"
 # setuptools-rust is a build dependency of cryptography, and since we don't have
 # a great way of pinning build dependencies, we simply list it here to ensure a
 # working version. Note: if build dependencies of setuptools-rust break at some
diff --git a/tools/pinning/oldest/pyproject.toml b/tools/pinning/oldest/pyproject.toml
index 67b835551ab..dc695e4fc8e 100644
--- a/tools/pinning/oldest/pyproject.toml
+++ b/tools/pinning/oldest/pyproject.toml
@@ -10,7 +10,7 @@ license = "Apache License 2.0"
 [tool.poetry.dependencies]
 # The Python version here should be kept in sync with the one used in our
 # oldest tests in tox.ini.
-python = "3.7"
+python = "3.7.*"
 
 # Local dependencies
 # Any local packages that have dependencies on other local packages must be
diff --git a/tools/requirements.txt b/tools/requirements.txt
index f79c3adc22f..1a1562cf5ea 100644
--- a/tools/requirements.txt
+++ b/tools/requirements.txt
@@ -5,187 +5,181 @@
 # requirements.txt so that is scanned by GitHub. See
 # https://docs.github.com/en/github/visualizing-repository-data-with-graphs/about-the-dependency-graph#supported-package-ecosystems
 # for more info.
-alabaster==0.7.12; python_version >= "3.7"
-apacheconfig==0.3.2; python_version >= "3.7"
-appdirs==1.4.4; python_version >= "3.7" and python_version < "4.0"
-appnope==0.1.3; python_version >= "3.7" and sys_platform == "darwin"
-astroid==2.11.7; python_version >= "3.7"
-attrs==22.1.0; python_version >= "3.7"
-awscli==1.25.76
-awscli==1.25.76; python_version >= "3.7"
-azure-devops==6.0.0b4; python_version >= "3.7"
-babel==2.10.3; python_version >= "3.7"
-backcall==0.2.0; python_version >= "3.7"
-bcrypt==4.0.0; python_version >= "3.7"
-beautifulsoup4==4.11.1; python_version >= "3.7"
-bleach==5.0.1; python_version >= "3.7"
-boto3==1.24.75; python_version >= "3.7"
-botocore==1.27.75; python_version >= "3.7"
-cachecontrol==0.12.12; python_version >= "3.7" and python_version < "4.0"
-cached-property==1.5.2; python_version < "3.8" and python_version >= "3.7"
-cachetools==5.2.0; python_version >= "3.7" and python_version < "4.0"
-cachy==0.3.0; python_version >= "3.7" and python_version < "4.0"
-certifi==2022.9.14; python_version >= "3.7" and python_version < "4" or python_version >= "3.7"
-cffi==1.15.1; python_version >= "3.7"
-charset-normalizer==2.1.1; python_version >= "3.7" and python_version < "4"
-cleo==1.0.0a5; python_version >= "3.7" and python_version < "4.0"
-cloudflare==2.10.1; python_version >= "3.7"
-colorama==0.4.4; python_version >= "3.7"
-configargparse==1.5.3; python_version >= "3.7"
-configobj==5.0.6; python_version >= "3.7"
-coverage==6.4.4; python_version >= "3.7"
-crashtest==0.3.1; python_version >= "3.7" and python_version < "4.0"
-cryptography==38.0.1
-cryptography==38.0.1; python_version >= "3.7"
-cython==0.29.32
-decorator==5.1.1; python_version >= "3.7"
-dill==0.3.5.1; python_version >= "3.7"
-distlib==0.3.6; python_version >= "3.7"
-distro==1.7.0; python_version >= "3.7"
-dns-lexicon==3.11.1; python_version >= "3.7" and python_version < "4.0"
-dnspython==2.2.1; python_version >= "3.7" and python_version < "4.0"
-docker-compose==1.29.2; python_version >= "3.7"
-docker==6.0.0; python_version >= "3.7"
-dockerpty==0.4.1; python_version >= "3.7"
-docopt==0.6.2; python_version >= "3.7"
-docutils==0.16; python_version >= "3.7"
-entrypoints==0.3; python_version >= "3.7" and python_version < "4.0"
-execnet==1.9.0; python_version >= "3.7"
-fabric==2.7.1; python_version >= "3.7"
-filelock==3.8.0; python_version >= "3.7" or python_version >= "3.7" and python_version < "4.0"
-google-api-core==2.10.1; python_version >= "3.7"
-google-api-python-client==2.61.0; python_version >= "3.7"
-google-auth-httplib2==0.1.0; python_version >= "3.7"
-google-auth==2.11.0; python_version >= "3.7"
-googleapis-common-protos==1.56.4; python_version >= "3.7"
-html5lib==1.1; python_version >= "3.7" and python_version < "4.0"
-httplib2==0.20.4; python_version >= "3.7"
-idna==3.4; python_version >= "3.7" and python_version < "4" or python_version >= "3.7" and python_version < "4.0"
-imagesize==1.4.1; python_version >= "3.7"
-importlib-metadata==1.7.0; python_version >= "3.7" and python_version < "3.8"
-iniconfig==1.1.1; python_version >= "3.7"
-invoke==1.7.1; python_version >= "3.7"
-ipdb==0.13.9; python_version >= "3.7"
-ipython==7.34.0; python_version >= "3.7"
-isodate==0.6.1; python_version >= "3.7"
-isort==5.10.1; python_version >= "3.7" and python_version < "4.0"
-jedi==0.18.1; python_version >= "3.7"
-jeepney==0.8.0; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux"
-jinja2==3.1.2; python_version >= "3.7"
-jmespath==1.0.1; python_version >= "3.7"
-josepy==1.13.0; python_version >= "3.7"
-jsonlines==3.1.0; python_version >= "3.7"
-jsonpickle==2.2.0; python_version >= "3.7"
-jsonschema==3.2.0; python_version >= "3.7"
-keyring==22.3.0; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7"
-lazy-object-proxy==1.7.1; python_version >= "3.7"
-markupsafe==2.1.1; python_version >= "3.7"
-matplotlib-inline==0.1.6; python_version >= "3.7"
-mccabe==0.7.0; python_version >= "3.7"
-msgpack==1.0.4; python_version >= "3.7" and python_version < "4.0"
-msrest==0.6.21; python_version >= "3.7"
-mypy-extensions==0.4.3; python_version >= "3.7"
-mypy==0.971; python_version >= "3.7"
-oauth2client==4.1.3; python_version >= "3.7"
-oauthlib==3.2.1; python_version >= "3.7"
-packaging==20.9; python_version >= "3.7"
-paramiko==2.11.0; python_version >= "3.7"
-parsedatetime==2.6; python_version >= "3.7"
-parso==0.8.3; python_version >= "3.7"
-pathlib2==2.3.7.post1; python_version >= "3.7"
-pexpect==4.8.0; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7" and sys_platform != "win32"
-pickleshare==0.7.5; python_version >= "3.7"
-pip==22.2.2; python_version >= "3.7"
-pkginfo==1.8.3; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7"
-platformdirs==2.5.2; python_version >= "3.7"
-pluggy==1.0.0; python_version >= "3.7"
-ply==3.11; python_version >= "3.7"
-poetry-core==1.1.0a7
-poetry==1.2.0a2
-prompt-toolkit==3.0.31; python_version >= "3.7"
-protobuf==4.21.6; python_version >= "3.7"
-ptyprocess==0.7.0; python_version >= "3.7" and python_version < "4.0"
-py==1.11.0; python_version >= "3.7"
-pyasn1-modules==0.2.8; python_version >= "3.7"
-pyasn1==0.4.8; python_version >= "3.7"
-pycparser==2.21; python_version >= "3.7"
-pygments==2.13.0; python_version >= "3.7"
-pylev==1.4.0; python_version >= "3.7" and python_version < "4.0"
-pylint==2.13.9
-pynacl==1.5.0; python_version >= "3.7"
-pynsist==2.7; python_version >= "3.7"
-pyopenssl==22.0.0; python_version >= "3.7"
-pyparsing==3.0.9; python_version >= "3.7"
-pyrfc3339==1.1; python_version >= "3.7"
-pyrsistent==0.18.1; python_version >= "3.7"
-pytest-cov==3.0.0; python_version >= "3.7"
-pytest-forked==1.4.0; python_version >= "3.7"
-pytest-xdist==2.5.0; python_version >= "3.7"
-pytest==7.1.3; python_version >= "3.7"
-python-augeas==1.1.0; python_version >= "3.7"
-python-dateutil==2.8.2; python_version >= "3.7"
-python-digitalocean==1.17.0; python_version >= "3.7"
-python-dotenv==0.21.0; python_version >= "3.7"
-pytz==2022.2.1; python_version >= "3.7"
-pywin32-ctypes==0.2.0; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
-pywin32==304; sys_platform == "win32" and python_version >= "3.7"
-pyyaml==5.4.1; python_version >= "3.7"
-readme-renderer==37.1; python_version >= "3.7"
-requests-download==0.1.2; python_version >= "3.7"
-requests-file==1.5.1; python_version >= "3.7" and python_version < "4.0"
-requests-oauthlib==1.3.1; python_version >= "3.7"
-requests-toolbelt==0.9.1; python_version >= "3.7"
-requests==2.28.1; python_version >= "3.7" and python_version < "4"
-rfc3986==2.0.0; python_version >= "3.7"
-rsa==4.7.2; python_version >= "3.7" and python_version < "4"
-s3transfer==0.6.0; python_version >= "3.7"
-secretstorage==3.3.3; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux"
-semantic-version==2.10.0; python_version >= "3.7"
-setuptools-rust==1.5.1
-setuptools==65.3.0; python_version >= "3.7"
-shellingham==1.5.0; python_version >= "3.7" and python_version < "4.0"
-six==1.16.0; python_version >= "3.7"
-snowballstemmer==2.2.0; python_version >= "3.7"
-soupsieve==2.3.2.post1; python_version >= "3.7"
-sphinx-rtd-theme==1.0.0; python_version >= "3.7"
-sphinx==4.3.2; python_version >= "3.7"
-sphinxcontrib-applehelp==1.0.2; python_version >= "3.7"
-sphinxcontrib-devhelp==1.0.2; python_version >= "3.7"
-sphinxcontrib-htmlhelp==2.0.0; python_version >= "3.7"
-sphinxcontrib-jsmath==1.0.1; python_version >= "3.7"
-sphinxcontrib-qthelp==1.0.3; python_version >= "3.7"
-sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.7"
-texttable==1.6.4; python_version >= "3.7"
-tldextract==3.3.1; python_version >= "3.7" and python_version < "4.0"
-toml==0.10.2; python_version >= "3.7"
-tomli==2.0.1; python_version < "3.11" and python_version >= "3.7" or python_full_version <= "3.11.0a6" and python_version >= "3.7" or python_version >= "3.7"
-tomlkit==0.11.4; python_version >= "3.7" and python_version < "4.0"
-tox==3.26.0; python_version >= "3.7"
-tqdm==4.64.1; python_version >= "3.7"
-traitlets==5.4.0; python_version >= "3.7"
-twine==3.3.0; python_version >= "3.7"
-typed-ast==1.5.4; python_version >= "3.7" and python_version < "3.8" or implementation_name == "cpython" and python_version < "3.8" and python_version >= "3.7"
-types-cryptography==3.3.23; python_version >= "3.7"
-types-pyopenssl==22.0.10; python_version >= "3.7"
-types-pyrfc3339==1.1.1; python_version >= "3.7"
-types-python-dateutil==2.8.19; python_version >= "3.7"
-types-pytz==2022.2.1.0; python_version >= "3.7"
-types-requests==2.28.10; python_version >= "3.7"
-types-setuptools==65.3.0; python_version >= "3.7"
-types-six==1.16.19; python_version >= "3.7"
-types-urllib3==1.26.24; python_version >= "3.7"
-typing-extensions==4.3.0; python_version >= "3.7" or python_version < "3.10" and python_version >= "3.7" or python_version < "3.8" and python_version >= "3.7"
-uritemplate==4.1.1; python_version >= "3.7"
-urllib3==1.26.12; python_version >= "3.7" and python_version < "4"
-virtualenv==20.4.4; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7"
-wcwidth==0.2.5; python_version >= "3.7"
-webencodings==0.5.1; python_version >= "3.7" and python_version < "4.0" or python_version >= "3.7"
-websocket-client==0.59.0; python_version >= "3.7"
-wheel==0.37.1; python_version >= "3.7"
-wrapt==1.14.1; python_version >= "3.7"
-yarg==0.1.9; python_version >= "3.7"
-zipp==3.8.1; python_version >= "3.7" and python_version < "3.8"
-zope.component==5.0.1; python_version >= "3.7"
-zope.event==4.5.0; python_version >= "3.7"
-zope.hookable==5.2; python_version >= "3.7"
-zope.interface==5.4.0; python_version >= "3.7"
+alabaster==0.7.12 ; python_version >= "3.7" and python_version < "4.0"
+apacheconfig==0.3.2 ; python_version >= "3.7" and python_version < "4.0"
+appnope==0.1.3 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "darwin"
+astroid==2.11.7 ; python_version >= "3.7" and python_version < "4.0"
+attrs==22.1.0 ; python_version >= "3.7" and python_version < "4.0"
+awscli==1.27.7 ; python_version >= "3.7" and python_version < "4.0"
+azure-devops==6.0.0b4 ; python_version >= "3.7" and python_version < "4.0"
+babel==2.11.0 ; python_version >= "3.7" and python_version < "4.0"
+backcall==0.2.0 ; python_version >= "3.7" and python_version < "4.0"
+backports-cached-property==1.0.2 ; python_version >= "3.7" and python_version < "3.8"
+bcrypt==4.0.1 ; python_version >= "3.7" and python_version < "4.0"
+beautifulsoup4==4.11.1 ; python_version >= "3.7" and python_version < "4.0"
+bleach==5.0.1 ; python_version >= "3.7" and python_version < "4.0"
+boto3==1.26.7 ; python_version >= "3.7" and python_version < "4.0"
+botocore==1.29.7 ; python_version >= "3.7" and python_version < "4.0"
+cachecontrol==0.12.11 ; python_version >= "3.7" and python_version < "4.0"
+cachetools==5.2.0 ; python_version >= "3.7" and python_version < "4.0"
+cachy==0.3.0 ; python_version >= "3.7" and python_version < "4.0"
+certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4"
+cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0"
+charset-normalizer==2.1.1 ; python_version >= "3.7" and python_version < "4"
+cleo==1.0.0a5 ; python_version >= "3.7" and python_version < "4.0"
+cloudflare==2.10.4 ; python_version >= "3.7" and python_version < "4.0"
+colorama==0.4.4 ; python_version >= "3.7" and python_version < "4.0"
+commonmark==0.9.1 ; python_version >= "3.7" and python_version < "4.0"
+configargparse==1.5.3 ; python_version >= "3.7" and python_version < "4.0"
+configobj==5.0.6 ; python_version >= "3.7" and python_version < "4.0"
+coverage==6.5.0 ; python_version >= "3.7" and python_version < "4.0"
+crashtest==0.3.1 ; python_version >= "3.7" and python_version < "4.0"
+cryptography==38.0.3 ; python_version >= "3.7" and python_version < "4.0"
+cython==0.29.32 ; python_version >= "3.7" and python_version < "4.0"
+decorator==5.1.1 ; python_version >= "3.7" and python_version < "4.0"
+dill==0.3.6 ; python_version >= "3.7" and python_version < "4.0"
+distlib==0.3.6 ; python_version >= "3.7" and python_version < "4.0"
+distro==1.8.0 ; python_version >= "3.7" and python_version < "4.0"
+dns-lexicon==3.11.7 ; python_version >= "3.7" and python_version < "4.0"
+dnspython==2.2.1 ; python_version >= "3.7" and python_version < "4.0"
+docutils==0.16 ; python_version >= "3.7" and python_version < "4.0"
+dulwich==0.20.50 ; python_version >= "3.7" and python_version < "4.0"
+exceptiongroup==1.0.1 ; python_version >= "3.7" and python_version < "3.11"
+execnet==1.9.0 ; python_version >= "3.7" and python_version < "4.0"
+fabric==2.7.1 ; python_version >= "3.7" and python_version < "4.0"
+filelock==3.8.0 ; python_version >= "3.7" and python_version < "4.0"
+google-api-core==2.10.2 ; python_version >= "3.7" and python_version < "4.0"
+google-api-python-client==2.65.0 ; python_version >= "3.7" and python_version < "4.0"
+google-auth-httplib2==0.1.0 ; python_version >= "3.7" and python_version < "4.0"
+google-auth==2.14.1 ; python_version >= "3.7" and python_version < "4.0"
+googleapis-common-protos==1.56.4 ; python_version >= "3.7" and python_version < "4.0"
+html5lib==1.1 ; python_version >= "3.7" and python_version < "4.0"
+httplib2==0.21.0 ; python_version >= "3.7" and python_version < "4.0"
+idna==3.4 ; python_version >= "3.7" and python_version < "4"
+imagesize==1.4.1 ; python_version >= "3.7" and python_version < "4.0"
+importlib-metadata==4.13.0 ; python_version >= "3.7" and python_version < "4.0"
+importlib-resources==5.10.0 ; python_version >= "3.7" and python_version < "3.9"
+iniconfig==1.1.1 ; python_version >= "3.7" and python_version < "4.0"
+invoke==1.7.3 ; python_version >= "3.7" and python_version < "4.0"
+ipdb==0.13.9 ; python_version >= "3.7" and python_version < "4.0"
+ipython==7.34.0 ; python_version >= "3.7" and python_version < "4.0"
+isodate==0.6.1 ; python_version >= "3.7" and python_version < "4.0"
+isort==5.10.1 ; python_version >= "3.7" and python_version < "4.0"
+jaraco-classes==3.2.3 ; python_version >= "3.7" and python_version < "4.0"
+jedi==0.18.1 ; python_version >= "3.7" and python_version < "4.0"
+jeepney==0.8.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux"
+jinja2==3.1.2 ; python_version >= "3.7" and python_version < "4.0"
+jmespath==1.0.1 ; python_version >= "3.7" and python_version < "4.0"
+josepy==1.13.0 ; python_version >= "3.7" and python_version < "4.0"
+jsonlines==3.1.0 ; python_version >= "3.7" and python_version < "4.0"
+jsonpickle==2.2.0 ; python_version >= "3.7" and python_version < "4.0"
+jsonschema==4.17.0 ; python_version >= "3.7" and python_version < "4.0"
+keyring==23.11.0 ; python_version >= "3.7" and python_version < "4.0"
+lazy-object-proxy==1.8.0 ; python_version >= "3.7" and python_version < "4.0"
+lockfile==0.12.2 ; python_version >= "3.7" and python_version < "4.0"
+markupsafe==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
+matplotlib-inline==0.1.6 ; python_version >= "3.7" and python_version < "4.0"
+mccabe==0.7.0 ; python_version >= "3.7" and python_version < "4.0"
+more-itertools==9.0.0 ; python_version >= "3.7" and python_version < "4.0"
+msgpack==1.0.4 ; python_version >= "3.7" and python_version < "4.0"
+msrest==0.6.21 ; python_version >= "3.7" and python_version < "4.0"
+mypy-extensions==0.4.3 ; python_version >= "3.7" and python_version < "4.0"
+mypy==0.990 ; python_version >= "3.7" and python_version < "4.0"
+oauth2client==4.1.3 ; python_version >= "3.7" and python_version < "4.0"
+oauthlib==3.2.2 ; python_version >= "3.7" and python_version < "4.0"
+packaging==21.3 ; python_version >= "3.7" and python_version < "4.0"
+paramiko==2.12.0 ; python_version >= "3.7" and python_version < "4.0"
+parsedatetime==2.6 ; python_version >= "3.7" and python_version < "4.0"
+parso==0.8.3 ; python_version >= "3.7" and python_version < "4.0"
+pathlib2==2.3.7.post1 ; python_version >= "3.7" and python_version < "4.0"
+pexpect==4.8.0 ; python_version >= "3.7" and python_version < "4.0"
+pickleshare==0.7.5 ; python_version >= "3.7" and python_version < "4.0"
+pip==22.3.1 ; python_version >= "3.7" and python_version < "4.0"
+pkginfo==1.8.3 ; python_version >= "3.7" and python_version < "4.0"
+pkgutil-resolve-name==1.3.10 ; python_version >= "3.7" and python_version < "3.9"
+platformdirs==2.5.3 ; python_version >= "3.7" and python_version < "4.0"
+pluggy==1.0.0 ; python_version >= "3.7" and python_version < "4.0"
+ply==3.11 ; python_version >= "3.7" and python_version < "4.0"
+poetry-core==1.3.2 ; python_version >= "3.7" and python_version < "4.0"
+poetry-plugin-export==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
+poetry==1.2.2 ; python_version >= "3.7" and python_version < "4.0"
+prompt-toolkit==3.0.32 ; python_version >= "3.7" and python_version < "4.0"
+protobuf==4.21.9 ; python_version >= "3.7" and python_version < "4.0"
+ptyprocess==0.7.0 ; python_version >= "3.7" and python_version < "4.0"
+py==1.11.0 ; python_version >= "3.7" and python_version < "4.0"
+pyasn1-modules==0.2.8 ; python_version >= "3.7" and python_version < "4.0"
+pyasn1==0.4.8 ; python_version >= "3.7" and python_version < "4.0"
+pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
+pygments==2.13.0 ; python_version >= "3.7" and python_version < "4.0"
+pylev==1.4.0 ; python_version >= "3.7" and python_version < "4.0"
+pylint==2.13.9 ; python_version >= "3.7" and python_version < "4.0"
+pynacl==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
+pynsist==2.7 ; python_version >= "3.7" and python_version < "4.0"
+pyopenssl==22.1.0 ; python_version >= "3.7" and python_version < "4.0"
+pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0"
+pyrfc3339==1.1 ; python_version >= "3.7" and python_version < "4.0"
+pyrsistent==0.19.2 ; python_version >= "3.7" and python_version < "4.0"
+pytest-cov==4.0.0 ; python_version >= "3.7" and python_version < "4.0"
+pytest-xdist==3.0.2 ; python_version >= "3.7" and python_version < "4.0"
+pytest==7.2.0 ; python_version >= "3.7" and python_version < "4.0"
+python-augeas==1.1.0 ; python_version >= "3.7" and python_version < "4.0"
+python-dateutil==2.8.2 ; python_version >= "3.7" and python_version < "4.0"
+python-digitalocean==1.17.0 ; python_version >= "3.7" and python_version < "4.0"
+pytz==2022.6 ; python_version >= "3.7" and python_version < "4.0"
+pywin32-ctypes==0.2.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
+pywin32==305 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
+pyyaml==5.4.1 ; python_version >= "3.7" and python_version < "4.0"
+readme-renderer==37.3 ; python_version >= "3.7" and python_version < "4.0"
+requests-download==0.1.2 ; python_version >= "3.7" and python_version < "4.0"
+requests-file==1.5.1 ; python_version >= "3.7" and python_version < "4.0"
+requests-oauthlib==1.3.1 ; python_version >= "3.7" and python_version < "4.0"
+requests-toolbelt==0.9.1 ; python_version >= "3.7" and python_version < "4.0"
+requests==2.28.1 ; python_version >= "3.7" and python_version < "4"
+rfc3986==2.0.0 ; python_version >= "3.7" and python_version < "4.0"
+rich==12.6.0 ; python_version >= "3.7" and python_version < "4.0"
+rsa==4.7.2 ; python_version >= "3.7" and python_version < "4"
+s3transfer==0.6.0 ; python_version >= "3.7" and python_version < "4.0"
+secretstorage==3.3.3 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux"
+semantic-version==2.10.0 ; python_version >= "3.7" and python_version < "4.0"
+setuptools-rust==1.5.2 ; python_version >= "3.7" and python_version < "4.0"
+setuptools==65.5.1 ; python_version >= "3.7" and python_version < "4.0"
+shellingham==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
+six==1.16.0 ; python_version >= "3.7" and python_version < "4.0"
+snowballstemmer==2.2.0 ; python_version >= "3.7" and python_version < "4.0"
+soupsieve==2.3.2.post1 ; python_version >= "3.7" and python_version < "4.0"
+sphinx-rtd-theme==1.1.1 ; python_version >= "3.7" and python_version < "4.0"
+sphinx==5.1.1 ; python_version >= "3.7" and python_version < "4.0"
+sphinxcontrib-applehelp==1.0.2 ; python_version >= "3.7" and python_version < "4.0"
+sphinxcontrib-devhelp==1.0.2 ; python_version >= "3.7" and python_version < "4.0"
+sphinxcontrib-htmlhelp==2.0.0 ; python_version >= "3.7" and python_version < "4.0"
+sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.7" and python_version < "4.0"
+sphinxcontrib-qthelp==1.0.3 ; python_version >= "3.7" and python_version < "4.0"
+sphinxcontrib-serializinghtml==1.1.5 ; python_version >= "3.7" and python_version < "4.0"
+tldextract==3.4.0 ; python_version >= "3.7" and python_version < "4.0"
+toml==0.10.2 ; python_version >= "3.7" and python_version < "4.0"
+tomli==2.0.1 ; python_version >= "3.7" and python_full_version <= "3.11.0a6"
+tomlkit==0.11.6 ; python_version >= "3.7" and python_version < "4.0"
+tox==3.27.0 ; python_version >= "3.7" and python_version < "4.0"
+traitlets==5.5.0 ; python_version >= "3.7" and python_version < "4.0"
+twine==4.0.1 ; python_version >= "3.7" and python_version < "4.0"
+typed-ast==1.5.4 ; python_version >= "3.7" and python_version < "3.8"
+types-cryptography==3.3.23.2 ; python_version >= "3.7" and python_version < "4.0"
+types-pyopenssl==22.1.0.2 ; python_version >= "3.7" and python_version < "4.0"
+types-pyrfc3339==1.1.1 ; python_version >= "3.7" and python_version < "4.0"
+types-python-dateutil==2.8.19.3 ; python_version >= "3.7" and python_version < "4.0"
+types-pytz==2022.6.0.1 ; python_version >= "3.7" and python_version < "4.0"
+types-requests==2.28.11.4 ; python_version >= "3.7" and python_version < "4.0"
+types-setuptools==65.5.0.2 ; python_version >= "3.7" and python_version < "4.0"
+types-six==1.16.21.2 ; python_version >= "3.7" and python_version < "4.0"
+types-urllib3==1.26.25.3 ; python_version >= "3.7" and python_version < "4.0"
+typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "4.0"
+uritemplate==4.1.1 ; python_version >= "3.7" and python_version < "4.0"
+urllib3==1.26.12 ; python_version >= "3.7" and python_version < "4"
+virtualenv==20.16.6 ; python_version >= "3.7" and python_version < "4.0"
+wcwidth==0.2.5 ; python_version >= "3.7" and python_version < "4.0"
+webencodings==0.5.1 ; python_version >= "3.7" and python_version < "4.0"
+wheel==0.38.4 ; python_version >= "3.7" and python_version < "4.0"
+wrapt==1.14.1 ; python_version >= "3.7" and python_version < "4.0"
+xattr==0.9.9 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "darwin"
+yarg==0.1.9 ; python_version >= "3.7" and python_version < "4.0"
+zipp==3.10.0 ; python_version >= "3.7" and python_version < "4.0"
diff --git a/tools/snap/README.md b/tools/snap/README.md
index e11b07db28f..f6354f7488c 100644
--- a/tools/snap/README.md
+++ b/tools/snap/README.md
@@ -17,7 +17,7 @@ These steps need to be done once to set up your VM and do not need to be run aga
  5. Add your current user to the lxd group and update your shell to have the new assignment by running `sudo usermod -a -G lxd ${USER} && newgrp lxd`.
  6. Install snapcraft with `sudo snap install --classic snapcraft`.
  7. `cd ~` (or any other directory where you want our source files to be)
- 8. Run `git clone git://github.com/certbot/certbot`
+ 8. Run `git clone https://github.com/certbot/certbot`
  9. `cd certbot` (All further instructions are relative to this directory.)
 
 ### Certbot Snap
@@ -80,7 +80,7 @@ It is easiest to run this from a local machine.
  2. Install git and python with `sudo apt update && sudo apt install -y git python`.
  3. Install snapcraft with `sudo snap install --classic snapcraft`.
  4. `cd ~` (or any other directory where you want our source files to be)
- 5. Run `git clone git://github.com/certbot/certbot`
+ 5. Run `git clone https://github.com/certbot/certbot`
  6. `cd certbot` (All further instructions are relative to this directory.)
  7. To trigger `snapcraft` to request access to your Launchpad account, run
     `snapcraft remote-build --launchpad-accept-public-upload --status`. A URL where you need


Test patch:
diff --git a/certbot-apache/tests/centos_test.py b/certbot-apache/tests/centos_test.py
index cc295266fd6..3f8e88467fd 100644
--- a/certbot-apache/tests/centos_test.py
+++ b/certbot-apache/tests/centos_test.py
@@ -5,8 +5,8 @@
 from certbot import errors
 from certbot.compat import filesystem
 from certbot.compat import os
-from certbot_apache._internal import obj
 from certbot_apache._internal import override_centos
+from certbot_apache._internal import obj
 import util
 
 
@@ -84,19 +84,66 @@ def test_fedora_restart(self):
                                 ['systemctl', 'restart', 'httpd'])
 
 
+class UseCorrectApacheExecutableTest(util.ApacheTest):
+    """Make sure the various CentOS/RHEL versions use the right httpd executable"""
+
+    def setUp(self):  # pylint: disable=arguments-differ
+        test_dir = "centos7_apache/apache"
+        config_root = "centos7_apache/apache/httpd"
+        vhost_root = "centos7_apache/apache/httpd/conf.d"
+        super().setUp(test_dir=test_dir,
+                      config_root=config_root,
+                      vhost_root=vhost_root)
+
+    @mock.patch("certbot.util.get_os_info")
+    def test_old_centos_rhel_and_fedora(self, mock_get_os_info):
+        for os_info in [("centos", "7"), ("rhel", "7"), ("fedora", "28"), ("scientific", "6")]:
+            mock_get_os_info.return_value = os_info
+            config = util.get_apache_configurator(
+                self.config_path, self.vhost_path, self.config_dir, self.work_dir,
+                os_info="centos")
+            self.assertEqual(config.options.ctl, "apachectl")
+            self.assertEqual(config.options.bin, "httpd")
+            self.assertEqual(config.options.version_cmd, ["apachectl", "-v"])
+            self.assertEqual(config.options.restart_cmd, ["apachectl", "graceful"])
+            self.assertEqual(config.options.restart_cmd_alt, ["apachectl", "restart"])
+            self.assertEqual(config.options.conftest_cmd, ["apachectl", "configtest"])
+            self.assertEqual(config.options.get_defines_cmd, ["apachectl", "-t", "-D", "DUMP_RUN_CFG"])
+            self.assertEqual(config.options.get_includes_cmd, ["apachectl", "-t", "-D", "DUMP_INCLUDES"])
+            self.assertEqual(config.options.get_modules_cmd, ["apachectl", "-t", "-D", "DUMP_MODULES"])
+
+    @mock.patch("certbot.util.get_os_info")
+    def test_new_rhel_derived(self, mock_get_os_info):
+        for os_info in [("centos", "9"), ("rhel", "9"), ("oracle", "9")]:
+            mock_get_os_info.return_value = os_info
+            config = util.get_apache_configurator(
+                self.config_path, self.vhost_path, self.config_dir, self.work_dir,
+                os_info=os_info[0])
+            self.assertEqual(config.options.ctl, "apachectl")
+            self.assertEqual(config.options.bin, "httpd")
+            self.assertEqual(config.options.version_cmd, ["httpd", "-v"])
+            self.assertEqual(config.options.restart_cmd, ["apachectl", "graceful"])
+            self.assertEqual(config.options.restart_cmd_alt, ["apachectl", "restart"])
+            self.assertEqual(config.options.conftest_cmd, ["apachectl", "configtest"])
+            self.assertEqual(config.options.get_defines_cmd, ["httpd", "-t", "-D", "DUMP_RUN_CFG"])
+            self.assertEqual(config.options.get_includes_cmd, ["httpd", "-t", "-D", "DUMP_INCLUDES"])
+            self.assertEqual(config.options.get_modules_cmd, ["httpd", "-t", "-D", "DUMP_MODULES"])
+
+
 class MultipleVhostsTestCentOS(util.ApacheTest):
     """Multiple vhost tests for CentOS / RHEL family of distros"""
 
     _multiprocess_can_split_ = True
 
-    def setUp(self):  # pylint: disable=arguments-differ
+    @mock.patch("certbot.util.get_os_info")
+    def setUp(self, mock_get_os_info):  # pylint: disable=arguments-differ
         test_dir = "centos7_apache/apache"
         config_root = "centos7_apache/apache/httpd"
         vhost_root = "centos7_apache/apache/httpd/conf.d"
         super().setUp(test_dir=test_dir,
                       config_root=config_root,
                       vhost_root=vhost_root)
-
+        mock_get_os_info.return_value = ("centos", "9")
         self.config = util.get_apache_configurator(
             self.config_path, self.vhost_path, self.config_dir, self.work_dir,
             os_info="centos")
@@ -120,9 +167,9 @@ def test_opportunistic_httpd_runtime_parsing(self, mock_get):
         )
         def mock_get_cfg(command):
             """Mock httpd process stdout"""
-            if command == ['apachectl', '-t', '-D', 'DUMP_RUN_CFG']:
+            if command == ['httpd', '-t', '-D', 'DUMP_RUN_CFG']:
                 return define_val
-            elif command == ['apachectl', '-t', '-D', 'DUMP_MODULES']:
+            elif command == ['httpd', '-t', '-D', 'DUMP_MODULES']:
                 return mod_val
             return ""
         mock_get.side_effect = mock_get_cfg
@@ -131,7 +178,7 @@ def mock_get_cfg(command):
 
         with mock.patch("certbot.util.get_os_info") as mock_osi:
             # Make sure we have the have the CentOS httpd constants
-            mock_osi.return_value = ("centos", "7")
+            mock_osi.return_value = ("centos", "9")
             self.config.parser.update_runtime_variables()
 
         self.assertEqual(mock_get.call_count, 3)
@@ -166,7 +213,7 @@ def test_get_sysconfig_vars(self, mock_cfg):
 
         with mock.patch("certbot.util.get_os_info") as mock_osi:
             # Make sure we have the have the CentOS httpd constants
-            mock_osi.return_value = ("centos", "7")
+            mock_osi.return_value = ("centos", "9")
             self.config.parser.update_runtime_variables()
 
         self.assertIn("mock_define", self.config.parser.variables)
diff --git a/certbot-apache/tests/util.py b/certbot-apache/tests/util.py
index cf97d9e7a18..7cea90f25d0 100644
--- a/certbot-apache/tests/util.py
+++ b/certbot-apache/tests/util.py
@@ -119,6 +119,7 @@ def get_apache_configurator(
                         # Custom virtualhost path was requested
                         config.config.apache_vhost_root = conf_vhost_path
                     config.config.apache_ctl = config_class.OS_DEFAULTS.ctl
+                    config.config.apache_bin = config_class.OS_DEFAULTS.bin
                     config.prepare()
     return config
 
diff --git a/certbot-ci/certbot_integration_tests/certbot_tests/context.py b/certbot-ci/certbot_integration_tests/certbot_tests/context.py
index 0dc73288082..6b7407e50d3 100644
--- a/certbot-ci/certbot_integration_tests/certbot_tests/context.py
+++ b/certbot-ci/certbot_integration_tests/certbot_tests/context.py
@@ -17,8 +17,8 @@ def __init__(self, request: pytest.FixtureRequest) -> None:
         self.request = request
 
         if hasattr(request.config, 'workerinput'):  # Worker node
-            self.worker_id = request.config.workerinput['workerid']  # type: ignore[attr-defined]
-            acme_xdist = request.config.workerinput['acme_xdist']  # type: ignore[attr-defined]
+            self.worker_id = request.config.workerinput['workerid']
+            acme_xdist = request.config.workerinput['acme_xdist']
         else:  # Primary node
             self.worker_id = 'primary'
             acme_xdist = request.config.acme_xdist  # type: ignore[attr-defined]
diff --git a/certbot-ci/certbot_integration_tests/nginx_tests/context.py b/certbot-ci/certbot_integration_tests/nginx_tests/context.py
index d52501596ab..9dd633db292 100644
--- a/certbot-ci/certbot_integration_tests/nginx_tests/context.py
+++ b/certbot-ci/certbot_integration_tests/nginx_tests/context.py
@@ -9,6 +9,7 @@
 from certbot_integration_tests.certbot_tests import context as certbot_context
 from certbot_integration_tests.nginx_tests import nginx_config as config
 from certbot_integration_tests.utils import certbot_call
+from certbot_integration_tests.utils import constants
 from certbot_integration_tests.utils import misc
 
 
@@ -65,4 +66,4 @@ def _start_nginx(self, default_server: bool) -> subprocess.Popen:
     def _stop_nginx(self) -> None:
         assert self.process.poll() is None
         self.process.terminate()
-        self.process.wait()
+        self.process.wait(constants.MAX_SUBPROCESS_WAIT)
diff --git a/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py b/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py
index dde41f367e6..4a383fe564f 100644
--- a/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py
+++ b/certbot-ci/certbot_integration_tests/rfc2136_tests/context.py
@@ -21,7 +21,7 @@ def __init__(self, request: pytest.FixtureRequest) -> None:
         self.request = request
 
         if hasattr(request.config, 'workerinput'):  # Worker node
-            self._dns_xdist = request.config.workerinput['dns_xdist']  # type: ignore[attr-defined]
+            self._dns_xdist = request.config.workerinput['dns_xdist']
         else:  # Primary node
             self._dns_xdist = request.config.dns_xdist  # type: ignore[attr-defined]
 
diff --git a/certbot-ci/certbot_integration_tests/utils/acme_server.py b/certbot-ci/certbot_integration_tests/utils/acme_server.py
index 8994d0ef0e3..c12057cd466 100755
--- a/certbot-ci/certbot_integration_tests/utils/acme_server.py
+++ b/certbot-ci/certbot_integration_tests/utils/acme_server.py
@@ -18,6 +18,7 @@
 from typing import List
 from typing import Mapping
 from typing import Optional
+from typing import Tuple
 from typing import Type
 
 import requests
@@ -63,6 +64,7 @@ def __init__(self, acme_server: str, nodes: List[str], http_proxy: bool = True,
         self._stdout = sys.stdout if stdout else open(os.devnull, 'w') # pylint: disable=consider-using-with
         self._dns_server = dns_server
         self._http_01_port = http_01_port
+        self._preterminate_cmds_args: List[Tuple[Tuple[Any, ...], Dict[str, Any]]] = []
         if http_01_port != DEFAULT_HTTP_01_PORT:
             if self._acme_type != 'pebble' or self._proxy:
                 raise ValueError('setting http_01_port is not currently supported '
@@ -85,6 +87,7 @@ def stop(self) -> None:
         """Stop the test stack, and clean its resources"""
         print('=> Tear down the test infrastructure...')
         try:
+            self._run_preterminate_cmds()
             for process in self._processes:
                 try:
                     process.terminate()
@@ -94,17 +97,7 @@ def stop(self) -> None:
                     if e.errno != errno.ESRCH:
                         raise
             for process in self._processes:
-                process.wait()
-
-            if os.path.exists(os.path.join(self._workspace, 'boulder')):
-                # Boulder docker generates build artifacts owned by root with 0o744 permissions.
-                # If we started the acme server from a normal user that has access to the Docker
-                # daemon, this user will not be able to delete these artifacts from the host.
-                # We need to do it through a docker.
-                process = self._launch_process(['docker', 'run', '--rm', '-v',
-                                                '{0}:/workspace'.format(self._workspace),
-                                                'alpine', 'rm', '-rf', '/workspace/boulder'])
-                process.wait()
+                process.wait(MAX_SUBPROCESS_WAIT)
         finally:
             if os.path.exists(self._workspace):
                 shutil.rmtree(self._workspace)
@@ -187,7 +180,7 @@ def _prepare_boulder_server(self) -> None:
         # Load Boulder from git, that includes a docker-compose.yml ready for production.
         process = self._launch_process(['git', 'clone', 'https://github.com/letsencrypt/boulder',
                                         '--single-branch', '--depth=1', instance_path])
-        process.wait()
+        process.wait(MAX_SUBPROCESS_WAIT)
 
         # Allow Boulder to ignore usual limit rate policies, useful for tests.
         os.rename(join(instance_path, 'test/rate-limit-policies-b.yml'),
@@ -202,6 +195,17 @@ def _prepare_boulder_server(self) -> None:
                 with open(join(instance_path, 'test/config/va{}.json'.format(suffix)), 'w') as f:
                     f.write(json.dumps(config, indent=2, separators=(',', ': ')))
 
+        # This command needs to be run before we try and terminate running processes because
+        # docker-compose up doesn't always respond to SIGTERM. See
+        # https://github.com/certbot/certbot/pull/9435.
+        self._register_preterminate_cmd(['docker-compose', 'down'], cwd=instance_path)
+        # Boulder docker generates build artifacts owned by root with 0o744 permissions.
+        # If we started the acme server from a normal user that has access to the Docker
+        # daemon, this user will not be able to delete these artifacts from the host.
+        # We need to do it through a docker.
+        self._register_preterminate_cmd(['docker', 'run', '--rm', '-v',
+                                         '{0}:/workspace'.format(self._workspace), 'alpine', 'rm',
+                                         '-rf', '/workspace/boulder'])
         try:
             # Launch the Boulder server
             self._launch_process(['docker-compose', 'up', '--force-recreate'], cwd=instance_path)
@@ -224,7 +228,7 @@ def _prepare_boulder_server(self) -> None:
             process = self._launch_process([
                 'docker-compose', 'logs'], cwd=instance_path, force_stderr=True
             )
-            process.wait()
+            process.wait(MAX_SUBPROCESS_WAIT)
             raise
 
         print('=> Finished boulder instance deployment.')
@@ -253,6 +257,17 @@ def _launch_process(self, command: List[str], cwd: str = os.getcwd(),
         self._processes.append(process)
         return process
 
+    def _register_preterminate_cmd(self, *args: Any, **kwargs: Any) -> None:
+        self._preterminate_cmds_args.append((args, kwargs))
+
+    def _run_preterminate_cmds(self) -> None:
+        for args, kwargs in self._preterminate_cmds_args:
+            process = self._launch_process(*args, **kwargs)
+            process.wait(MAX_SUBPROCESS_WAIT)
+        # It's unlikely to matter, but let's clear the list of cleanup commands
+        # once they've been run.
+        self._preterminate_cmds_args.clear()
+
 
 def main() -> None:
     # pylint: disable=missing-function-docstring
diff --git a/certbot-ci/certbot_integration_tests/utils/constants.py b/certbot-ci/certbot_integration_tests/utils/constants.py
index a788881ef45..ce0cd91d567 100644
--- a/certbot-ci/certbot_integration_tests/utils/constants.py
+++ b/certbot-ci/certbot_integration_tests/utils/constants.py
@@ -9,3 +9,4 @@
 PEBBLE_CHALLTESTSRV_URL = f'http://localhost:{CHALLTESTSRV_PORT}'
 MOCK_OCSP_SERVER_PORT = 4002
 PEBBLE_ALTERNATE_ROOTS = 2
+MAX_SUBPROCESS_WAIT = 120
diff --git a/certbot-ci/certbot_integration_tests/utils/dns_server.py b/certbot-ci/certbot_integration_tests/utils/dns_server.py
index 7dfc9c0b248..ed5b6ccaef7 100644
--- a/certbot-ci/certbot_integration_tests/utils/dns_server.py
+++ b/certbot-ci/certbot_integration_tests/utils/dns_server.py
@@ -17,6 +17,8 @@
 
 from pkg_resources import resource_filename
 
+from certbot_integration_tests.utils import constants
+
 BIND_DOCKER_IMAGE = "internetsystemsconsortium/bind9:9.16"
 BIND_BIND_ADDRESS = ("127.0.0.1", 45953)
 
@@ -67,7 +69,7 @@ def stop(self) -> None:
         if self.process:
             try:
                 self.process.terminate()
-                self.process.wait()
+                self.process.wait(constants.MAX_SUBPROCESS_WAIT)
             except BaseException as e:
                 print("BIND9 did not stop cleanly: {}".format(e), file=sys.stderr)
 
diff --git a/certbot/certbot/tests/util.py b/certbot/certbot/tests/util.py
index 3509f360f7d..772fcd5a5c4 100644
--- a/certbot/certbot/tests/util.py
+++ b/certbot/certbot/tests/util.py
@@ -39,7 +39,7 @@
 class DummyInstaller(common.Installer):
     """Dummy installer plugin for test purpose."""
     def get_all_names(self) -> Iterable[str]:
-        pass
+        return []
 
     def deploy_cert(self, domain: str, cert_path: str, key_path: str, chain_path: str,
                     fullchain_path: str) -> None:
@@ -50,7 +50,7 @@ def enhance(self, domain: str, enhancement: str,
         pass
 
     def supported_enhancements(self) -> List[str]:
-        pass
+        return []
 
     def save(self, title: Optional[str] = None, temporary: bool = False) -> None:
         pass
@@ -69,7 +69,7 @@ def prepare(self) -> None:
         pass
 
     def more_info(self) -> str:
-        pass
+        return ""
 
 
 def vector_path(*names: str) -> str:
@@ -253,7 +253,7 @@ class FreezableMock:
     value of func is ignored.
 
     """
-    def __init__(self, frozen: bool = False, func: Callable[..., Any] = None,
+    def __init__(self, frozen: bool = False, func: Optional[Callable[..., Any]] = None,
                  return_value: Any = mock.sentinel.DEFAULT) -> None:
         self._frozen_set = set() if frozen else {'freeze', }
         self._func = func
diff --git a/pytest.ini b/pytest.ini
index 704912685da..22beaa54e8a 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -16,7 +16,10 @@
 # 2) botocore's default TLS settings raise deprecation warnings in Python
 #    3.10+, but their values are sane from a security perspective. See
 #    https://github.com/boto/botocore/issues/2550.
+# 3) pytest-cov uses deprecated functionality in pytest-xdist, to be resolved by
+#    https://github.com/pytest-dev/pytest-cov/issues/557.
 filterwarnings =
     error
     ignore:decodestring\(\) is a deprecated alias:DeprecationWarning:dns
     ignore:'urllib3.contrib.pyopenssl:DeprecationWarning:botocore
+    ignore:.*rsyncdir:DeprecationWarning